11use std:: collections:: { HashMap , HashSet } ;
2+ use std:: hash:: { Hash , Hasher } ;
23use std:: io:: { BufRead , BufReader as StdBufReader , IsTerminal , Read , Write } ;
34use std:: path:: { Path , PathBuf } ;
45use 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+
10681099async 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+
14271499struct 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+
33263431fn 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+
37083823struct 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 \n User 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