MoonshotAI / MoonshotAI/kimi-cli
Extreme typing latency in inline modal inputs (approval feedback / question Other)
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:
buffer.start_completion()triggers filesystem-scanning completers- The entire Rich panel is re-rendered to ANSI via a new
Consoleinstance per keystroke - 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
- Start any agent turn that produces an approval request or
AskUserQuestion - Select the option that enables inline typing:
- Approval panel → "Reject, tell the model what to do instead" (option 4)
- Question panel → "Other" option
- Type any text into the hidden input buffer
- 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
-
Disable
complete_while_typingduring modal inline input — whenrunning_prompt_hides_input_buffer() == True, the buffer is being used as a hidden text field, not a command line. Completions should not run. -
Reuse or cache
Consoleinstances inrender_to_ansi— Creating a newrich.Consoleper call is extremely expensive. Consider a module-levelWeakKeyDictionarycache keyed by(columns, theme)or useconsole.render_lines()against the existingconsoleinstance. -
Add memoization to delegate renderers —
ApprovalPromptDelegateandQuestionPromptDelegate(and potentially_BtwModalDelegate) should cache their last ANSI output and only re-render when state actually changes. -
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.08srefresh loop for its spinner.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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