Skip to content

fix(repl): blank prompt prefix during suppressed-spinner dispatch (#2…#2551

Open
Davidson3556 wants to merge 2 commits into
Tracer-Cloud:mainfrom
Davidson3556:fix/repl-spinner-overlaps-footer-2116
Open

fix(repl): blank prompt prefix during suppressed-spinner dispatch (#2…#2551
Davidson3556 wants to merge 2 commits into
Tracer-Cloud:mainfrom
Davidson3556:fix/repl-spinner-overlaps-footer-2116

Conversation

@Davidson3556
Copy link
Copy Markdown
Contributor

@Davidson3556 Davidson3556 commented May 26, 2026

Fixes #2116

Describe the changes you have made in this PR -

The REPL prompt's "reserved first row" was falling back to idle_hint_ansi() (/ for commands · ↑↓ history) on every prompt_async refresh tick (every 0.25s) — including while a dispatch was running with the streaming spinner suppressed. The spinner is suppressed whenever a nested live renderer (the Rich Live investigation footer or the append-only progress display) is about to own the bottom of the terminal. With the spinner off, the prompt kept redrawing shortcut text on the same rows the investigation footer was animating on, which produced the visible flash reported in #2116.

Changes:

  • app/cli/interactive_shell/runtime/loop.py
    • Extracted the prompt-message composition out of the nested _message_with_spinner closure into a module-level helper _build_prompt_message(session, state, spinner). The closure now just delegates, so the prompt_async(message=…) wiring is unchanged.
    • New rule for the prefix slot: when a dispatch is in flight and the spinner has been suppressed, the slot stays blank instead of falling back to the idle hint. Confirmation banners and the streaming spinner still take priority. Idle behavior (no dispatch running) is unchanged.
    • Slot height stays exactly one row in every state, so prompt-toolkit's cursor management continues to work and stale spinner lines do not accumulate.
  • tests/cli/interactive_shell/test_terminal_runtime.py

Demo/Screenshot for feature changes and bug fixes -

before
image

after
image

before
image

after
image


Code Understanding and AI Usage

Did you use AI assistance (ChatGPT, Claude, Copilot, etc.) to write any part of this code?

  • No, I wrote all the code myself
  • Yes, I used AI assistance (continue below)

If you used AI assistance:

  • I have reviewed every single line of the AI-generated code
  • I can explain the purpose and logic of each function/component I added
  • I have tested edge cases and understand how the code handles them
  • I have modified the AI output to follow this project's coding standards and conventions

Explain your implementation approach:

The bug is a race between two independent renderers writing to the same terminal rows:

  1. The REPL's prompt_async redraws its "message" every PROMPT_REFRESH_INTERVAL_S (0.25s) so the spinner can animate. The message is built by _message_with_spinner, which historically returned inline_spinner_ansi() or idle_hint_ansi() as the prefix above the prompt rule.
  2. During an investigation triggered from a paste-alert turn, a nested progress renderer takes over and console.suppress_prompt_spinner() is called. That sets spinner.streaming = False, so on the next prompt refresh the prefix silently fell back to the idle hint — and that hint is what the user saw flashing against the live footer.

Alternatives I considered:

  • Stop prompt_async entirely during dispatches. Rejected — confirmation prompts, follow-up typing, and esc to cancel all rely on the prompt staying live. PR fix(repl): improve /investigate picker UX and restore live stream #2470 already split exclusive vs non-exclusive stdin paths for the same reason.
  • Lower refresh_interval during dispatch. Rejected — refresh_interval is set once at prompt_async call time and isn't safe to mutate dynamically; and we still need 0.25s ticks for the streaming spinner animation in pure chat turns.
  • Remove the idle hint entirely. Rejected — when no dispatch is running it's a useful affordance for new users.

I chose the minimum-surface fix: keep the existing idle hint, but treat "dispatch running with spinner suppressed" as a fourth explicit state where the prefix is blank. This is symmetric with how is_awaiting_confirmation() already takes over the same slot. The helper was lifted out of the closure so the four-state logic is unit-testable directly, not just observable via integration tests of run_interactive.

Key components I added:

  • _build_prompt_message(session, state, spinner) -> ANSI — pure function over the runtime state. Returns the confirmation banner, the streaming spinner, an empty prefix, or the idle hint, in that priority order. Always 1 row tall.
  • TestBuildPromptMessage — exercises each branch. The regression test for [BUG] REPL spinner flashes and overlaps investigation footer during live RCA #2116 is test_dispatch_running_with_suppressed_spinner_uses_blank_prefix, which asserts that neither / for commands nor the streaming verbs leak into the prefix when a dispatch is in flight without an active spinner.

Edge cases checked:

  • Confirmation during dispatch — the y/N banner still wins over the blank prefix (test_confirmation_takes_priority_over_dispatch_blank).
  • Dispatch with the spinner still active — the streaming verb is still shown, so pure chat turns are unaffected (test_dispatch_running_with_spinner_keeps_spinner).
  • Slot height — verified to be exactly one row in idle, streaming, and dispatch-suppressed states (test_prefix_row_height_is_constant) so prompt-toolkit's renderer doesn't misplace the cursor between transitions.
  • Bare /investigate — already routed via dispatch_needs_exclusive_stdin, so prompt_async is parked at await state.queue.join() during the picker + investigation. This path is unaffected and continues to use the full Rich Live region.

Gate results on this branch:

  • make lint — passed
  • make format-check — passed
  • make typecheck — passed (687 source files)
  • uv run pytest tests/cli/ non-live — 1584 passed, 27 deselected (the deselected cases are test_live_llm_planner_matches_prompt_contract, which require OPENAI_API_KEY and fail identically on main)

Checklist before requesting a review

  • I have added proper PR title and linked to the issue
  • I have performed a self-review of my code
  • I can explain the purpose of every function, class, and logic block I added
  • I understand why my changes work and have tested them thoroughly
  • I have considered potential edge cases and how my code handles them
  • If it is a core feature, I have added thorough tests
  • My code follows the project's style guidelines and conventions

Note: Please check Allow edits from maintainers if you would like us to assist in the PR.

…acer-Cloud#2116)

While a nested live renderer (Rich Live investigation footer or the
append-only progress display) owns the bottom of the terminal during a
dispatch, the REPL prompt was still falling back to ``idle_hint_ansi()``
on every refresh tick.  The shortcut text fought the investigation
footer for the same row and produced a visible flash.

Extract the prompt-message composition into a module-level
``_build_prompt_message`` helper and keep the prefix slot blank when a
dispatch is running with the spinner suppressed.  Confirmation banners
and the streaming spinner still take priority; idle behavior is
unchanged.  Slot height stays one row in every state so prompt-toolkit's
cursor management remains accurate.
@github-actions
Copy link
Copy Markdown
Contributor

Greptile code review

This repo uses Greptile for automated review. Before merge, aim for Confidence Score: 5/5 with zero unresolved review threads — see CONTRIBUTING.md.

Run a review — add a PR comment with:

@greptile review

Give it ~5-10 minutes (sometimes longer) for results, then fix feedback and re-trigger until you reach Confidence Score: 5/5.

Optional: automate with the greploop skill.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 26, 2026

Greptile Summary

This PR fixes a terminal rendering race (#2116) where prompt_async was redrawing the idle-hint shortcut text on every 0.25 s refresh tick even when a nested Rich Live renderer had taken over the terminal footer, causing a visible flash against the investigation progress display.

  • Root cause fix in loop.py: The old _message_with_spinner closure used spinner.inline_spinner_ansi() or spinner.idle_hint_ansi(), which fell back to the idle hint whenever streaming=False — including during dispatches where the spinner had been explicitly suppressed. The refactored _build_prompt_message adds a fourth explicit state: dispatch running + spinner suppressed → blank prefix, stopping the flash without removing the idle hint for genuine idle states.
  • Test coverage in test_terminal_runtime.py: Six new unit tests in TestBuildPromptMessage exercise every branch of the four-state priority chain, including the specific regression scenario and the constant-row-height invariant that prompt_toolkit's cursor management relies on.

Confidence Score: 5/5

Safe to merge — the change is a targeted, additive fix to a single pure function with no state side effects, and all four prefix states are unit-tested.

The refactor is minimal: one new module-level pure function replaces an equivalent inline closure, and the only behavioral change is a new blank-prefix branch for the dispatch running + spinner suppressed state. Both the original priority order and the one-row height invariant are preserved and verified by the new tests. No public API changes, no shared-state mutations, and all is untouched.

No files require special attention.

Important Files Changed

Filename Overview
app/cli/interactive_shell/runtime/loop.py Extracts _build_prompt_message from the _message_with_spinner closure and adds a fourth state (dispatch running + spinner suppressed → blank prefix) to fix the #2116 flash regression. Logic and priority ordering are correct; __all__ does not expose the private helper.
tests/cli/interactive_shell/test_terminal_runtime.py Adds TestBuildPromptMessage with 6 unit cases covering all four prompt-prefix states: idle, streaming, dispatch+active spinner, dispatch+suppressed spinner (regression case), confirmation priority, and constant one-row prefix height.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_build_prompt_message called\n(every 0.25s refresh tick)"] --> B{is_awaiting_confirmation?}
    B -- Yes --> C["Return: confirm_prompt_text + base\n(y/N banner)"]
    B -- No --> D{spinner.streaming?}
    D -- Yes --> E["Return: inline_spinner_ansi + base\n(pondering… animation)"]
    D -- No --> F{is_dispatch_running?}
    F -- "Yes (spinner suppressed\nby nested live renderer)" --> G["Return: '' + base\n(blank prefix — #2116 fix)"]
    F -- No --> H["Return: idle_hint_ansi + base\n('/ for commands · ↑↓ history')"]

    style G fill:#d4f1c0,stroke:#4caf50
    style C fill:#fff3cd,stroke:#f0ad4e
    style E fill:#cce5ff,stroke:#004085
    style H fill:#f8d7da,stroke:#721c24
Loading

Reviews (2): Last reviewed commit: "fix(repl): address greptile review on pr..." | Re-trigger Greptile

Comment thread app/cli/interactive_shell/runtime/loop.py Outdated
Comment thread app/cli/interactive_shell/runtime/loop.py Outdated
- Promote the blank-prefix branch to a peer of the other three priority-list
  items in the ``_build_prompt_message`` docstring so the four-state ordering
  reads at a glance instead of relying on a trailing prose paragraph.
- Drop ``_build_prompt_message`` from ``__all__``.  The leading underscore
  already marks it module-private; the tests import it via attribute lookup
  on ``loop_module``, which works regardless of the export list, so the entry
  only created a contradiction between the name and the public surface.
@Davidson3556
Copy link
Copy Markdown
Contributor Author

@greptile review

@Davidson3556
Copy link
Copy Markdown
Contributor Author

@muddlebee please review

@cerencamkiran
Copy link
Copy Markdown
Collaborator

Looks good 👍

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.

[BUG] REPL spinner flashes and overlaps investigation footer during live RCA

2 participants