Skip to content

Commit f87bf7a

Browse files
Pyinerclaude
andcommitted
Drive Claude Code 2.1.x: fix trust-dialog detection and ACK-confirm submit
On Claude Code 2.1.x the gateway path (stream-json + MCP) hung because: - the workspace-trust dialog detector still required the old 'Quick safety check' title that 2.1.x dropped, so the dialog wasn't recognized/auto-acknowledged; and - submit_prompt_to_tty decided whether to press Enter from the on-screen state, unreliable when Claude is slow to become input-ready (MCP startup / right after the trust dialog) — the paste landed on a transitional screen, no Enter was sent, and the message was silently dropped. Recognize the trust dialog by its stable 'Yes, I trust this folder' affirmative, and confirm each submission via transcript activity (ACK), clearing the line and re-submitting if no ACK arrives in the window. Verified end-to-end on Linux (Claude Code 2.1.177): gateway cctty mode returns pong in a fresh untrusted dir; short -p still works. Adds a trust-dialog detection regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 96dfaca commit f87bf7a

1 file changed

Lines changed: 101 additions & 16 deletions

File tree

src/runner.rs

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
10741128
fn 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(
39524011
fn 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

39594027
fn 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\nContext 0% /mcp";

0 commit comments

Comments
 (0)