Skip to content

Commit 6e85b1e

Browse files
BunsDevclaude
andauthored
fix(tui): expand paste placeholders at submit time (#149)
handle_paste stores large pastes in paste_contents and inserts a [Pasted ~N lines #K] placeholder, but nothing ever read the store back: PromptInputState::take() returned the composer text verbatim, so the model received the literal placeholder instead of the pasted content. take() now rebuilds each placeholder from its stored content and expands it before returning; the placeholder format lives in one helper shared with handle_paste so the two cannot drift. clear() drops stored contents (placeholders live in text, so once text is cleared they are unreachable) while paste_counter stays monotonic to keep stale placeholder ids from colliding after undo. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 2ef1754 commit 6e85b1e

1 file changed

Lines changed: 86 additions & 3 deletions

File tree

src-rust/crates/tui/src/prompt_input.rs

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,8 +1789,18 @@ pub fn handle_paste(content: &str, paste_counter: &mut u32) -> (String, Option<S
17891789
return (content.to_string(), None);
17901790
}
17911791
*paste_counter += 1;
1792-
let placeholder = format!("[Pasted ~{} lines #{}]", line_count, paste_counter);
1793-
(placeholder, Some(content.to_string()))
1792+
(
1793+
paste_placeholder(line_count, *paste_counter),
1794+
Some(content.to_string()),
1795+
)
1796+
}
1797+
1798+
/// The placeholder shown in the input in place of a large paste.
1799+
///
1800+
/// `PromptInputState::take` rebuilds this exact string from the stored
1801+
/// content to expand placeholders at submit time, so the two must not drift.
1802+
fn paste_placeholder(line_count: usize, id: u32) -> String {
1803+
format!("[Pasted ~{line_count} lines #{id}]")
17941804
}
17951805

17961806
/// Normalize a pasted string into a filesystem path if it looks like one.
@@ -3148,11 +3158,26 @@ impl PromptInputState {
31483158
self.visual_anchor = None;
31493159
self.vim_command_buf.clear();
31503160
self.vim_search_buf.clear();
3161+
// Placeholders live in `text`, so once that is gone the stored paste
3162+
// contents are unreachable. `paste_counter` stays monotonic so a
3163+
// stale placeholder (e.g. restored via undo) can never collide with
3164+
// a later paste's id.
3165+
self.paste_contents.clear();
31513166
}
31523167

31533168
/// Take the current text, clearing the input.
3169+
///
3170+
/// Placeholders inserted by [`handle_paste`] are expanded back to their
3171+
/// stored paste contents, so callers receive what the user actually
3172+
/// pasted rather than the `[Pasted ~N lines #K]` marker.
31543173
pub fn take(&mut self) -> String {
3155-
let text = self.text.clone();
3174+
let mut text = std::mem::take(&mut self.text);
3175+
for (id, content) in &self.paste_contents {
3176+
let placeholder = paste_placeholder(content.lines().count(), *id);
3177+
if text.contains(&placeholder) {
3178+
text = text.replace(&placeholder, content);
3179+
}
3180+
}
31563181
self.clear();
31573182
text
31583183
}
@@ -4266,6 +4291,64 @@ mod tests {
42664291
assert_eq!(counter, 2);
42674292
}
42684293

4294+
#[test]
4295+
fn take_expands_paste_placeholders() {
4296+
let mut s = PromptInputState::new();
4297+
s.paste("look at this: ");
4298+
let big = "x".repeat(200);
4299+
s.paste(&big);
4300+
let taken = s.take();
4301+
assert!(
4302+
taken.contains(&big),
4303+
"placeholder should expand to the pasted content"
4304+
);
4305+
assert!(
4306+
!taken.contains("[Pasted ~"),
4307+
"no placeholder should survive take(): {taken}"
4308+
);
4309+
assert!(
4310+
s.paste_contents.is_empty(),
4311+
"paste store should be dropped after take()"
4312+
);
4313+
}
4314+
4315+
#[test]
4316+
fn take_expands_multiple_pastes() {
4317+
let mut s = PromptInputState::new();
4318+
let first = "a".repeat(160);
4319+
let second = "line\n".repeat(4);
4320+
s.paste(&first);
4321+
s.paste(" between ");
4322+
s.paste(&second);
4323+
let taken = s.take();
4324+
assert!(taken.contains(&first));
4325+
assert!(taken.contains(" between "));
4326+
assert!(taken.contains(&second));
4327+
assert!(!taken.contains("[Pasted ~"));
4328+
}
4329+
4330+
#[test]
4331+
fn take_drops_deleted_placeholder_content() {
4332+
let mut s = PromptInputState::new();
4333+
let big = "y".repeat(200);
4334+
s.paste(&big);
4335+
// User deleted the placeholder and typed something else instead —
4336+
// the stored content must not leak into the submitted text.
4337+
s.text = "typed instead".to_string();
4338+
s.cursor = s.text.len();
4339+
let taken = s.take();
4340+
assert_eq!(taken, "typed instead");
4341+
assert!(s.paste_contents.is_empty());
4342+
}
4343+
4344+
#[test]
4345+
fn clear_drops_paste_contents() {
4346+
let mut s = PromptInputState::new();
4347+
s.paste(&"z".repeat(200));
4348+
s.clear();
4349+
assert!(s.paste_contents.is_empty());
4350+
}
4351+
42694352
// ---- compute_typeahead ---------------------------------------------
42704353

42714354
// Helper constants for tests

0 commit comments

Comments
 (0)