@@ -1025,7 +1025,7 @@ async fn submit_prompt_and_tail(
10251025 include_partial_messages : bool ,
10261026) -> Result < TranscriptState > {
10271027 tail. prepare_offset ( ) ?;
1028- submit_prompt_to_tty ( process, prompt) . await ?;
1028+ submit_prompt_to_tty ( process, tail , prompt) . await ?;
10291029 tail_until_complete ( process, tail, output_format, include_partial_messages) . await
10301030}
10311031
@@ -1040,7 +1040,7 @@ async fn submit_prompt_and_tail_stream(
10401040 include_partial_messages : bool ,
10411041) -> Result < TranscriptState > {
10421042 tail. prepare_offset ( ) ?;
1043- submit_prompt_to_tty ( process, prompt) . await ?;
1043+ submit_prompt_to_tty ( process, tail , prompt) . await ?;
10441044 tail_until_complete_stream (
10451045 process,
10461046 tail,
@@ -1053,24 +1053,78 @@ async fn submit_prompt_and_tail_stream(
10531053 . await
10541054}
10551055
1056- async fn submit_prompt_to_tty ( process : & mut PtyProcess , prompt : & str ) -> Result < ( ) > {
1056+ async fn submit_prompt_to_tty (
1057+ process : & mut PtyProcess ,
1058+ tail : & mut TailCursor ,
1059+ prompt : & str ,
1060+ ) -> Result < ( ) > {
10571061 logging:: event ( format ! (
10581062 "prompt_submit start content_chars={}" ,
10591063 prompt. chars( ) . count( )
10601064 ) ) ;
1061- process. write_all ( & bracketed_paste_input ( prompt) ) ?;
1062- tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
1063- maybe_log_submit_tty_diagnostic ( process, "after_paste" ) ;
1064- if tty_output_still_editing_prompt ( & process. recent_output ( ) , prompt) {
1065- logging:: event ( "prompt_submit_retry reason=prompt_still_visible" ) ;
1066- process. write_all ( b"\r " ) ?;
1067- tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
1068- maybe_log_submit_tty_diagnostic ( process, "after_retry_enter" ) ;
1069- }
1070- logging:: event ( "prompt_submit done" ) ;
1065+ // Confirm the submission via the transcript (an ACK that Claude actually
1066+ // accepted the message) instead of trusting the on-screen state. When Claude
1067+ // is slow to become input-ready — connecting MCP servers, or right after the
1068+ // workspace-trust dialog is dismissed — the paste lands on a transitional
1069+ // screen, the "still editing" heuristic reads false, no Enter is sent, and
1070+ // the message is silently dropped. If no transcript activity appears within
1071+ // the window, clear the line and re-submit (always pressing Enter).
1072+ const ACK_TIMEOUT : Duration = Duration :: from_secs ( 5 ) ;
1073+ const MAX_ATTEMPTS : usize = 4 ;
1074+ for attempt in 0 ..MAX_ATTEMPTS {
1075+ if attempt == 0 {
1076+ process. write_all ( & bracketed_paste_input ( prompt) ) ?;
1077+ tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
1078+ maybe_log_submit_tty_diagnostic ( process, "after_paste" ) ;
1079+ if tty_output_still_editing_prompt ( & process. recent_output ( ) , prompt) {
1080+ process. write_all ( b"\r " ) ?;
1081+ tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
1082+ maybe_log_submit_tty_diagnostic ( process, "after_enter" ) ;
1083+ }
1084+ } else {
1085+ logging:: event ( format ! (
1086+ "prompt_submit_retry attempt={attempt} reason=no_transcript_ack"
1087+ ) ) ;
1088+ process. write_all ( b"\x1b " ) ?; // Esc: dismiss any stray menu
1089+ tokio:: time:: sleep ( Duration :: from_millis ( 80 ) ) . await ;
1090+ process. write_all ( b"\x15 " ) ?; // Ctrl-U: clear the input line
1091+ tokio:: time:: sleep ( Duration :: from_millis ( 80 ) ) . await ;
1092+ process. write_all ( & bracketed_paste_input ( prompt) ) ?;
1093+ tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
1094+ maybe_log_submit_tty_diagnostic ( process, "after_repaste" ) ;
1095+ process. write_all ( b"\r " ) ?;
1096+ tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
1097+ maybe_log_submit_tty_diagnostic ( process, "after_retry_enter" ) ;
1098+ }
1099+ if wait_for_transcript_ack ( tail, ACK_TIMEOUT ) . await ? {
1100+ logging:: event ( format ! ( "prompt_submit done attempt={attempt} ack=true" ) ) ;
1101+ return Ok ( ( ) ) ;
1102+ }
1103+ }
1104+ logging:: event ( "prompt_submit done ack=false" ) ;
10711105 Ok ( ( ) )
10721106}
10731107
1108+ /// Wait until the target transcript shows new bytes — Claude's acknowledgement
1109+ /// that it accepted the submitted prompt — or the timeout elapses.
1110+ async fn wait_for_transcript_ack ( tail : & mut TailCursor , timeout : Duration ) -> Result < bool > {
1111+ let deadline = Instant :: now ( ) + timeout;
1112+ loop {
1113+ if let Some ( path) = tail. resolve_path ( ) ? {
1114+ if std:: fs:: metadata ( & path)
1115+ . map ( |meta| meta. len ( ) > tail. offset )
1116+ . unwrap_or ( false )
1117+ {
1118+ return Ok ( true ) ;
1119+ }
1120+ }
1121+ if Instant :: now ( ) >= deadline {
1122+ return Ok ( false ) ;
1123+ }
1124+ tokio:: time:: sleep ( Duration :: from_millis ( 250 ) ) . await ;
1125+ }
1126+ }
1127+
10741128fn maybe_log_prompt_diagnostic ( prompt : & str ) {
10751129 if std:: env:: var ( "CCTTY_LOG_PROMPT" ) . ok ( ) . as_deref ( ) != Some ( "1" ) {
10761130 return ;
@@ -2985,7 +3039,12 @@ async fn cancel_tty_question(process: &mut PtyProcess, feedback: Option<&str>) -
29853039 process. write_all ( & bracketed_paste_input ( feedback) ) ?;
29863040 }
29873041 Some ( QuestionFeedbackTarget :: MainPrompt ) => {
2988- submit_prompt_to_tty ( process, feedback) . await ?;
3042+ process. write_all ( & bracketed_paste_input ( feedback) ) ?;
3043+ tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
3044+ if tty_output_still_editing_prompt ( & process. recent_output ( ) , feedback) {
3045+ process. write_all ( b"\r " ) ?;
3046+ tokio:: time:: sleep ( Duration :: from_millis ( 120 ) ) . await ;
3047+ }
29893048 }
29903049 None => { }
29913050 }
@@ -3952,8 +4011,17 @@ async fn prepare_tty_for_prompt_with_mcp(
39524011fn tty_output_has_workspace_trust_prompt ( output : & str ) -> bool {
39534012 let output = plain_tty_output ( output) ;
39544013 let compact = compact_tty_output ( & output) ;
3955- ( output. contains ( "Quick safety check" ) || compact. contains ( "Quicksafetycheck" ) )
3956- && ( output. contains ( "Yes, I trust this folder" ) || compact. contains ( "Yes,Itrustthisfolder" ) )
4014+ // "Yes, I trust this folder" is the stable affirmative across Claude Code
4015+ // versions. Older builds also rendered a "Quick safety check" title, but
4016+ // 2.1.x dropped it (the dialog now reads "...take a moment to review what's
4017+ // in this folder..." with a "Security guide" link). Requiring the old title
4018+ // left the 2.1.x trust dialog unrecognized, so startup stalled on it instead
4019+ // of auto-acknowledging. Match on the affirmative alone, keeping the legacy
4020+ // title as an additional accepted marker.
4021+ output. contains ( "Yes, I trust this folder" )
4022+ || compact. contains ( "Yes,Itrustthisfolder" )
4023+ || output. contains ( "Quick safety check" )
4024+ || compact. contains ( "Quicksafetycheck" )
39574025}
39584026
39594027fn tty_output_has_startup_choice_prompt ( output : & str ) -> bool {
@@ -4184,6 +4252,23 @@ mod tests {
41844252 ) ;
41854253 }
41864254
4255+ #[ test]
4256+ fn recognizes_workspace_trust_prompt_across_versions ( ) {
4257+ // Claude Code 2.1.x dialog — the old "Quick safety check" title is gone.
4258+ let v2 = "Do you trust the files in this folder? /tmp/x \
4259+ take a moment to review what's in this folder first. \
4260+ Claude Code'll be able to read, edit, and execute files here. Security guide \
4261+ ❯ 1. Yes, I trust this folder 2. No, suggest changes (esc)";
4262+ assert ! ( tty_output_has_workspace_trust_prompt( v2) ) ;
4263+ assert_eq ! ( classify_tty_screen( v2) , TtyScreenState :: WorkspaceTrustPrompt ) ;
4264+ // Legacy dialog with the old title still matches.
4265+ let legacy = "Quick safety check ❯ 1. Yes, I trust this folder 2. No" ;
4266+ assert ! ( tty_output_has_workspace_trust_prompt( legacy) ) ;
4267+ // A normal prompt-ready screen must not be misread as the trust dialog.
4268+ let ready = "❯ Try \" fix lint errors\" ? for shortcuts" ;
4269+ assert ! ( !tty_output_has_workspace_trust_prompt( ready) ) ;
4270+ }
4271+
41874272 #[ test]
41884273 fn detects_prompt_left_in_tty_input ( ) {
41894274 let output = "❯ Write a compact document for SDK users\n Context 0% /mcp" ;
0 commit comments