Skip to content

Commit b6fdcb3

Browse files
author
lijiuyang.5137
committed
Handle collapsed Claude paste prompts
1 parent 1aa7503 commit b6fdcb3

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

src/runner.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::{HashMap, HashSet};
2+
use std::hash::{Hash, Hasher};
23
use std::io::{BufRead, BufReader as StdBufReader, IsTerminal, Read, Write};
34
use std::path::{Path, PathBuf};
45
use std::process::Stdio;
@@ -534,6 +535,7 @@ async fn run_stream_json(
534535
"stream_user received content_chars={}",
535536
prompt.chars().count()
536537
));
538+
maybe_log_prompt_diagnostic(&prompt);
537539
if ensure_sdk_mcp_runtime(process, tail, &mut input, &mut sdk_state, spawn).await? {
538540
tty_prepared = true;
539541
}
@@ -1057,14 +1059,43 @@ async fn submit_prompt_to_tty(process: &mut PtyProcess, prompt: &str) -> Result<
10571059
));
10581060
process.write_all(&bracketed_paste_input(prompt))?;
10591061
tokio::time::sleep(Duration::from_millis(120)).await;
1062+
maybe_log_submit_tty_diagnostic(process, "after_paste");
10601063
if tty_output_still_editing_prompt(&process.recent_output(), prompt) {
10611064
logging::event("prompt_submit_retry reason=prompt_still_visible");
10621065
process.write_all(b"\r")?;
1066+
tokio::time::sleep(Duration::from_millis(120)).await;
1067+
maybe_log_submit_tty_diagnostic(process, "after_retry_enter");
10631068
}
10641069
logging::event("prompt_submit done");
10651070
Ok(())
10661071
}
10671072

1073+
fn maybe_log_prompt_diagnostic(prompt: &str) {
1074+
if std::env::var("CCTTY_LOG_PROMPT").ok().as_deref() != Some("1") {
1075+
return;
1076+
}
1077+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
1078+
prompt.hash(&mut hasher);
1079+
logging::event(format!(
1080+
"prompt_debug chars={} hash={:016x} preview={}",
1081+
prompt.chars().count(),
1082+
hasher.finish(),
1083+
single_line_log_text(&recent_tty_log_text(prompt, 1_000))
1084+
));
1085+
}
1086+
1087+
fn maybe_log_submit_tty_diagnostic(process: &PtyProcess, stage: &str) {
1088+
if std::env::var("CCTTY_LOG_TTY").ok().as_deref() != Some("1") {
1089+
return;
1090+
}
1091+
logging::event(format!(
1092+
"prompt_submit_tty stage={} tty={} text={}",
1093+
stage,
1094+
tty_wait_class(&process.recent_output()),
1095+
single_line_log_text(&recent_tty_log_text(&process.recent_output(), 1_500))
1096+
));
1097+
}
1098+
10681099
async fn tail_until_complete(
10691100
process: &mut PtyProcess,
10701101
tail: &mut TailCursor,
@@ -1075,6 +1106,7 @@ async fn tail_until_complete(
10751106
let mut last_activity = Instant::now();
10761107
let mut state = TranscriptState::default();
10771108
let mut tty_debug = TtyDebugLogger::new("text");
1109+
let mut tail_progress = TailProgressLogger::new("text");
10781110
let mut questions = TtyQuestionBridge::new(false);
10791111

10801112
loop {
@@ -1135,6 +1167,7 @@ async fn tail_until_complete(
11351167
emit_idle_session_state_if_requested(&mut state, output_format)?;
11361168
return Ok(state);
11371169
}
1170+
tail_progress.maybe_log(process, tail, &state, started.elapsed());
11381171
tty_debug.maybe_log(process, started.elapsed());
11391172
tokio::time::sleep(TRANSCRIPT_POLL).await;
11401173
}
@@ -1155,6 +1188,7 @@ async fn tail_until_complete_stream(
11551188
let mut permission = PermissionBridge::new(permission_prompt_tool_stdio);
11561189
let mut questions = TtyQuestionBridge::new(permission_prompt_tool_stdio);
11571190
let mut tty_debug = TtyDebugLogger::new("stream");
1191+
let mut tail_progress = TailProgressLogger::new("stream");
11581192

11591193
loop {
11601194
if started.elapsed() > RUN_TIMEOUT {
@@ -1237,6 +1271,7 @@ async fn tail_until_complete_stream(
12371271
emit_idle_session_state_if_requested(&mut state, output_format)?;
12381272
return Ok(state);
12391273
}
1274+
tail_progress.maybe_log(process, tail, &state, started.elapsed());
12401275
tty_debug.maybe_log(process, started.elapsed());
12411276
tokio::time::sleep(TRANSCRIPT_POLL).await;
12421277
}
@@ -1424,6 +1459,43 @@ impl TtyDebugLogger {
14241459
}
14251460
}
14261461

1462+
struct TailProgressLogger {
1463+
stage: &'static str,
1464+
next_log: Instant,
1465+
}
1466+
1467+
impl TailProgressLogger {
1468+
fn new(stage: &'static str) -> Self {
1469+
Self {
1470+
stage,
1471+
next_log: Instant::now() + Duration::from_secs(10),
1472+
}
1473+
}
1474+
1475+
fn maybe_log(
1476+
&mut self,
1477+
process: &PtyProcess,
1478+
tail: &TailCursor,
1479+
state: &TranscriptState,
1480+
elapsed: Duration,
1481+
) {
1482+
if Instant::now() < self.next_log {
1483+
return;
1484+
}
1485+
self.next_log = Instant::now() + Duration::from_secs(10);
1486+
logging::event(format!(
1487+
"tail_wait stage={} elapsed_ms={} transcript={} offset={} assistant_chars={} saw_result={} tty={}",
1488+
self.stage,
1489+
elapsed.as_millis(),
1490+
tail.path_log_label(),
1491+
tail.offset,
1492+
state.assistant_text.chars().count(),
1493+
state.saw_result,
1494+
tty_wait_class(&process.recent_output()),
1495+
));
1496+
}
1497+
}
1498+
14271499
struct PermissionBridge {
14281500
enabled: bool,
14291501
requested_tool_use_ids: HashSet<String>,
@@ -3323,6 +3395,39 @@ fn tty_output_has_plan_approval_prompt(output: &str) -> bool {
33233395
has_plan_language && has_question && has_allow_choice && has_deny_choice
33243396
}
33253397

3398+
fn tty_wait_class(output: &str) -> &'static str {
3399+
let plain = plain_tty_output(output);
3400+
let compact = compact_tty_output(&plain);
3401+
if compact.is_empty() {
3402+
return "empty";
3403+
}
3404+
if tty_output_has_plan_approval_prompt(output) {
3405+
return "plan_approval";
3406+
}
3407+
if tty_question_from_form(&plain).is_some() {
3408+
return "question_form";
3409+
}
3410+
if plain_tty_output_has_file_permission_prompt(&plain)
3411+
|| plain_tty_output_has_permission_prompt_for_tool(&plain, "Bash")
3412+
|| plain_tty_output_has_generic_permission_prompt(&plain)
3413+
{
3414+
return "permission_prompt";
3415+
}
3416+
if compact.contains("RemoteControlfailed") {
3417+
return "remote_control_failed";
3418+
}
3419+
if compact.contains("RemoteControlactive") {
3420+
return "remote_control_active";
3421+
}
3422+
if tty_output_accepts_prompt(output) {
3423+
return "prompt_ready";
3424+
}
3425+
if compact.contains("Esc") && compact.contains("interrupt") {
3426+
return "busy";
3427+
}
3428+
"other"
3429+
}
3430+
33263431
fn tty_plan_approval_prompt_has_feedback_choice(output: &str) -> bool {
33273432
let output = plain_tty_output(output);
33283433
let compact = compact_tty_output(&output);
@@ -3692,6 +3797,9 @@ fn tty_output_still_editing_prompt(output: &str, prompt: &str) -> bool {
36923797
{
36933798
return false;
36943799
}
3800+
if tty_output_has_collapsed_paste_input(&plain) {
3801+
return true;
3802+
}
36953803
let prompt = compact_tty_output(prompt);
36963804
if prompt.is_empty() {
36973805
return false;
@@ -3705,6 +3813,13 @@ fn tty_output_still_editing_prompt(output: &str, prompt: &str) -> bool {
37053813
compact_tty_output(&plain).contains(tail)
37063814
}
37073815

3816+
fn tty_output_has_collapsed_paste_input(output: &str) -> bool {
3817+
let recent = recent_tty_log_text(output, 1_200);
3818+
let compact = compact_tty_output(&recent);
3819+
(recent.contains("[Pasted text #") || compact.contains("[Pastedtext#"))
3820+
&& (recent.contains("paste again to expand") || compact.contains("pasteagaintoexpand"))
3821+
}
3822+
37083823
struct TailCursor {
37093824
path: Option<PathBuf>,
37103825
config_dir: PathBuf,
@@ -3775,6 +3890,15 @@ impl TailCursor {
37753890
self.session_alias.externalize_value(value);
37763891
}
37773892

3893+
fn path_log_label(&self) -> String {
3894+
self.path
3895+
.as_ref()
3896+
.and_then(|path| path.file_name())
3897+
.and_then(|file_name| file_name.to_str())
3898+
.unwrap_or("none")
3899+
.to_owned()
3900+
}
3901+
37783902
fn remove_current_transcript(&self) -> Result<()> {
37793903
let Some(path) = &self.path else {
37803904
return Ok(());
@@ -3863,6 +3987,31 @@ mod tests {
38633987
));
38643988
}
38653989

3990+
#[test]
3991+
fn detects_collapsed_paste_left_in_tty_input() {
3992+
let output = "\
3993+
[Opus 4.8] │ workspace git:( test ) Context 0% \
3994+
⏸ plan mode on (shift+tab to cycle) · ← for agents \
3995+
────── ↯ 1 MCP server failed · /mcp \
3996+
❯ B [Pasted text #1 +15 lines] paste again to expand";
3997+
assert!(tty_output_still_editing_prompt(
3998+
output,
3999+
"system instructions\n\nUser prompt"
4000+
));
4001+
}
4002+
4003+
#[test]
4004+
fn ignores_stale_collapsed_paste_from_previous_turn() {
4005+
let output = format!(
4006+
"[Pasted text #1 +15 lines] paste again to expand {} ❯",
4007+
"assistant result ".repeat(200)
4008+
);
4009+
assert!(!tty_output_still_editing_prompt(
4010+
&output,
4011+
"next user prompt"
4012+
));
4013+
}
4014+
38664015
#[test]
38674016
fn does_not_retry_submit_on_permission_prompt() {
38684017
let output =

0 commit comments

Comments
 (0)