fix(shell): persist pasted text placeholders#2388
Conversation
Store long pasted text through the prompt attachment cache so placeholder tokens can be resolved after history recall creates a fresh PromptPlaceholderManager. Keep legacy [Pasted text #n] tokens readable while preventing unresolved pasted text placeholders from being sent to the model verbatim; missing payloads now resolve to an explicit error message. Fixes MoonshotAI#1946 Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa47175f46
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if match.match.re is _PERSISTED_PASTED_TEXT_PLACEHOLDER_RE: | ||
| return match.raw |
There was a problem hiding this comment.
Expand persisted paste tokens outside history writes
When a long paste is entered into a running approval/question modal, those delegates pass serialize_for_history as their text_expander (src/kimi_cli/ui/shell/visualize/_approval_panel.py and _question_panel.py). With this branch returning the raw persisted token here, reject feedback or “Other” answers containing pasted text now submit [Pasted text:…] instead of the original content, whereas legacy in-memory tokens were expanded. Keep history preservation separate from the expansion path used by these modal callers.
Useful? React with 👍 / 👎.
| if match.match.re is _PERSISTED_PASTED_TEXT_PLACEHOLDER_RE: | ||
| return match.raw |
There was a problem hiding this comment.
🔴 serialize_for_history used as text_expander no longer expands persisted placeholders, sending raw tokens to the model
The PR changed serialize_for_history (line 352-353) to return match.raw for persisted placeholders, which is correct for the history serialization use case. However, serialize_for_history is also used as the text_expander callback in the approval panel (src/kimi_cli/ui/shell/__init__.py:1399) and question panel (src/kimi_cli/ui/shell/visualize/_interactive.py:514). These callers expect placeholders to be expanded to their full text content before sending to the model. Since persisted tokens are now the default (happy path), when a user pastes long text in a question or approval feedback field, the model receives the literal placeholder string [Pasted text:abc123.txt +15 lines] instead of the actual pasted content. The correct method to use as text_expander would be expand_for_editor, which always expands both legacy and persisted text placeholders via expand_text.
Prompt for agents
The issue is that serialize_for_history now preserves persisted placeholder tokens (returning match.raw), but it is also used as a text_expander callback in two places that need full expansion:
1. src/kimi_cli/ui/shell/__init__.py:1399 - ApprovalPromptDelegate text_expander
2. src/kimi_cli/ui/shell/visualize/_interactive.py:514 - QuestionPromptDelegate text_expander
Both of these call sites should use expand_for_editor instead of serialize_for_history as the text_expander, since expand_for_editor always expands text placeholders (both legacy and persisted) to their full content while keeping image tokens as-is. Change both sites from:
text_expander=self._prompt_session._get_placeholder_manager().serialize_for_history
to:
text_expander=self._prompt_session._get_placeholder_manager().expand_for_editor
Was this helpful? React with 👍 or 👎 to provide feedback.
Related Issue
Resolve #1946
Description
Long pasted text is folded into a placeholder such as
[Pasted text #1]before it appears in the prompt. That works only while the originalPastedTextPlaceholderHandlerstill has the in-memory entry. After prompt/session history recall, a newPromptPlaceholderManagercannot resolve the old entry, soresolve_command()falls back to sending the literal placeholder text to the model. That matches #1946: the model receives[Pasted text ...]instead of the pasted content.Fix
AttachmentCache, similar to image placeholders.[Pasted text:<id>.txt +N lines].PromptPlaceholderManagerusing the same cache root can resolve the token back to the original pasted text.[Pasted text #n]) still work within the same manager.Notes on PR size
This is a little over 100 changed lines because it keeps legacy placeholder compatibility while adding cache-backed pasted text. The related bug issue already exists and this stays scoped to
ui/shell/placeholders.pyplus focused prompt tests.Checklist
CHANGELOG.md(manual Unreleased entry, following the existing one-line-per-bullet style;make gen-changelognot run because this is a focused bug fix).make gen-docsnot run — N/A: no user-facing CLI/config documentation change.Test plan
UV_PYTHON=3.12 uv run ruff check src/kimi_cli/ui/shell/placeholders.py tests/ui_and_conv/test_prompt_placeholders.py tests/ui_and_conv/test_prompt_clipboard.py tests/ui_and_conv/test_prompt_history.py→ cleanUV_PYTHON=3.12 uv run ruff format --check src/kimi_cli/ui/shell/placeholders.py tests/ui_and_conv/test_prompt_placeholders.py tests/ui_and_conv/test_prompt_clipboard.py tests/ui_and_conv/test_prompt_history.py→ cleanUV_PYTHON=3.12 uv run pytest tests/ui_and_conv/test_prompt_placeholders.py tests/ui_and_conv/test_prompt_clipboard.py tests/ui_and_conv/test_prompt_history.py tests/ui_and_conv/test_prompt_external_editor.py -q→43 passedProof of fix
test_placeholder_manager_resolves_pasted_text_with_new_managercreates a long paste token with one manager, then resolves it with a fresh manager and the same cache root; the original text is sent, not the placeholder.test_placeholder_manager_uses_error_text_for_missing_persisted_textensures a missing cache entry does not leak the literal placeholder to the model.test_placeholder_manager_uses_error_text_for_unknown_legacy_text_tokencovers old[Pasted text #n]tokens when their in-memory entry is unavailable.Made with Cursor