MoonshotAI / MoonshotAI/kimi-cli

Extreme typing latency in inline modal inputs (approval feedback / question Other)

Open
#2,032 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
11.4k
Forks
1.3k
Avg merge
9h 47m
Merged PRs (30d)
2

Description

Extreme typing latency in inline modal inputs (approval feedback / question "Other")

Summary

When a modal inline input field is active (e.g. typing feedback into "Reject, tell the model what to do instead" or typing a custom answer in a question panel's "Other" option), every keystroke — including backspace — causes multi-second hangs. The UI becomes completely unresponsive during typing.

This happens because three expensive operations run synchronously on every single keystroke:

  1. buffer.start_completion() triggers filesystem-scanning completers
  2. The entire Rich panel is re-rendered to ANSI via a new Console instance per keystroke
  3. A background task forces full UI redraws at 10 Hz regardless of user activity

Environment

  • Terminal: Ghostty (macOS, GPU-accelerated)
  • Installation method: uv tool install kimi-cli
  • Python: 3.13
  • Reproducibility: 100% whenever an inline hidden-buffer input is active

Steps to Reproduce

  1. Start any agent turn that produces an approval request or AskUserQuestion
  2. Select the option that enables inline typing:
    • Approval panel → "Reject, tell the model what to do instead" (option 4)
    • Question panel → "Other" option
  3. Type any text into the hidden input buffer
  4. Observe that each character takes seconds to appear; backspace is equally slow

Root Cause Analysis

1. Completion engine fires on every text change
# ui/shell/prompt.py ~1500
@self._session.default_buffer.on_text_changed.add_handler
def _(buffer: Buffer) -> None:
    self._last_input_activity_time = time.monotonic()
    self._input_activity_event.set()
    if buffer.complete_while_typing() and not self._suppress_auto_completion:
        buffer.start_completion()  # <-- runs even for hidden passphrase/feedback input

Even though the user is typing a passphrase or feedback text (no / or @), start_completion() still evaluates LocalFileMentionCompleter and SlashCommandCompleter on every keystroke.

2. render_to_ansi() creates a brand-new rich.Console on every keystroke
# ui/shell/console.py ~94

def render_to_ansi(renderable: RenderableType, *, columns: int) -> str:
    buf = StringIO()
    temp = Console(          # <-- new Console every call
        file=buf,
        force_terminal=True,
        width=width,
        theme=NEUTRAL_MARKDOWN_THEME,
        highlight=False,
    )
    temp.print(renderable, end="")
    ...

Both ApprovalPromptDelegate.render_running_prompt_body() and QuestionPromptDelegate.render_running_prompt_body() call render_to_ansi() inside _render_agent_prompt_message(), which prompt_toolkit invokes on every UI redraw.

So the flow per keystroke is:

  • User types → buffer changes → app.invalidate()
  • Prompt toolkit redraws → _render_agent_prompt_message()
  • _render_interactive_body()delegate.render_running_prompt_body(columns)
  • render_to_ansi(panel.render(...), columns=columns)new Console + full panel render + ANSI conversion
3. Background refresh forces 10 Hz redraws continuously
# ui/shell/prompt.py ~1832
interval = (
    _RUNNING_REFRESH_INTERVAL  # 0.1s
    if self._active_prompt_delegate() is not None
    ...
)

While any modal delegate is attached (approval, question, btw), the background task calls app.invalidate() 10 times per second, even when the user isn't typing. Each invalidate triggers the expensive render chain above.

Local Fix Applied

I patched my local installation (~/.local/share/uv/tools/kimi-cli/) with three changes:

Patch 1: Skip completions when modal hides the buffer
delegate = self._active_prompt_delegate()
if delegate is not None and delegate.running_prompt_hides_input_buffer():
    return
buffer.start_completion()
Patch 2: Use idle refresh interval during modal hidden-input states
delegate = self._active_prompt_delegate()
interval = (
    _RUNNING_REFRESH_INTERVAL
    if (delegate is not None and not delegate.running_prompt_hides_input_buffer())
    or (self._fast_refresh_provider is not None and self._fast_refresh_provider())
    else _IDLE_REFRESH_INTERVAL
)
Patch 3: Cache Rich→ANSI output in approval/question delegates

Memoize render_running_prompt_body() with a cache key of (id(panel), columns, input_text, selected_index[, multi_selected]). This turns the 10 Hz background redraws into instant cache hits when the user isn't typing, and eliminates redundant re-renders between keystrokes.

After these patches, inline input feels instant in Ghostty.

Suggested Upstream Fixes

  1. Disable complete_while_typing during modal inline input — when running_prompt_hides_input_buffer() == True, the buffer is being used as a hidden text field, not a command line. Completions should not run.

  2. Reuse or cache Console instances in render_to_ansi — Creating a new rich.Console per call is extremely expensive. Consider a module-level WeakKeyDictionary cache keyed by (columns, theme) or use console.render_lines() against the existing console instance.

  3. Add memoization to delegate renderersApprovalPromptDelegate and QuestionPromptDelegate (and potentially _BtwModalDelegate) should cache their last ANSI output and only re-render when state actually changes.

  4. Throttle background refresh during static modals — Approval/question panels are largely static. They don't need 10 Hz redraws. The BTW modal already has its own 0.08s refresh loop for its spinner.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the text-change handler and refresh loop in ui/shell/prompt.py, then trace render_to_ansi() in ui/shell/console.py and the approval and question delegate renderers. Reproduce typing in the hidden inline inputs and verify that completion, ANSI rendering, and refresh work are avoided or cached when state is unchanged, while input remains responsive.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.