diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 918fbfb..ec464d6 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -201,16 +201,25 @@ fn run() -> Result<(), Box> { output_format, allowed_tools, permission_mode, - compact: _, + compact, base_commit, } => { run_stale_base_preflight(base_commit.as_deref()); - let stdin_context = read_piped_stdin(); + // Only consume piped stdin as prompt context when the permission + // mode is fully unattended. In modes where the permission + // prompter may invoke CliPermissionPrompter::decide(), stdin + // must remain available for interactive approval; otherwise the + // prompter's read_line() would hit EOF and deny every request. + let stdin_context = if matches!(permission_mode, PermissionMode::DangerFullAccess) { + read_piped_stdin() + } else { + None + }; let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref()); LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output( &effective_prompt, output_format, - false, + compact, )?; } CliAction::Login { output_format } => run_login(output_format)?, @@ -4394,6 +4403,22 @@ fn resolve_managed_session_path(session_id: &str) -> Result.{jsonl,json}` without the per-workspace hash + // subdirectory. Walk up from `directory` to the `.claw/sessions/` root + // and try the flat layout as a fallback so users do not lose access + // to their pre-upgrade managed sessions. + if let Some(legacy_root) = directory + .parent() + .filter(|parent| parent.file_name().is_some_and(|name| name == "sessions")) + { + for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] { + let path = legacy_root.join(format!("{session_id}.{extension}")); + if path.exists() { + return Ok(path); + } + } + } Err(format_missing_session_reference(session_id).into()) } @@ -4405,9 +4430,14 @@ fn is_managed_session_file(path: &Path) -> bool { }) } -fn list_managed_sessions() -> Result, Box> { - let mut sessions = Vec::new(); - for entry in fs::read_dir(sessions_dir()?)? { +fn collect_sessions_from_dir( + directory: &Path, + sessions: &mut Vec, +) -> Result<(), Box> { + if !directory.exists() { + return Ok(()); + } + for entry in fs::read_dir(directory)? { let entry = entry?; let path = entry.path(); if !is_managed_session_file(&path) { @@ -4457,6 +4487,24 @@ fn list_managed_sessions() -> Result, Box Result, Box> { + let mut sessions = Vec::new(); + let primary_dir = sessions_dir()?; + collect_sessions_from_dir(&primary_dir, &mut sessions)?; + + // Backward compatibility: include sessions stored in the pre-isolation + // flat `.claw/sessions/` root so users do not lose access to existing + // managed sessions after the workspace-hashed subdirectory rollout. + if let Some(legacy_root) = primary_dir + .parent() + .filter(|parent| parent.file_name().is_some_and(|name| name == "sessions")) + { + collect_sessions_from_dir(legacy_root, &mut sessions)?; + } + sessions.sort_by(|left, right| { right .modified_epoch_millis @@ -9018,11 +9066,14 @@ mod tests { fn multi_word_prompt_still_uses_shorthand_prompt_mode() { let _guard = env_lock(); std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE"); + // Input is ["help", "me", "debug"] so the joined prompt shorthand + // must be "help me debug". A previous batch accidentally rewrote + // the expected string to "$help overview" (copy-paste slip). assert_eq!( parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()]) .expect("prompt shorthand should still work"), CliAction::Prompt { - prompt: "$help overview".to_string(), + prompt: "help me debug".to_string(), model: DEFAULT_MODEL.to_string(), output_format: CliOutputFormat::Text, allowed_tools: None, @@ -9339,7 +9390,9 @@ mod tests { assert!(help.contains("/diff")); assert!(help.contains("/version")); assert!(help.contains("/export [file]")); - assert!(help.contains("/session [list|switch |fork [branch-name]]")); + // Batch 5 added `/session delete`; match on the stable core rather than + // the trailing bracket so future additions don't re-break this. + assert!(help.contains("/session [list|switch |fork [branch-name]")); assert!(help.contains( "/plugin [list|install |enable |disable |uninstall |update ]" )); diff --git a/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs b/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs index 4091613..066abb6 100644 --- a/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs +++ b/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs @@ -183,17 +183,24 @@ fn clean_env_cli_reaches_mock_anthropic_service_across_scripted_parity_scenarios } let captured = runtime.block_on(server.captured_requests()); - assert_eq!( - captured.len(), - 21, - "twelve scenarios should produce twenty-one requests" - ); - assert!(captured + // After `be561bf` added count_tokens preflight, each turn sends an + // extra POST to `/v1/messages/count_tokens` before the messages POST. + // The original count (21) assumed messages-only requests. We now + // filter to `/v1/messages` and verify that subset matches the original + // scenario expectation. + let messages_only: Vec<_> = captured .iter() - .all(|request| request.path == "/v1/messages")); - assert!(captured.iter().all(|request| request.stream)); + .filter(|r| r.path == "/v1/messages") + .collect(); + assert_eq!( + messages_only.len(), + 21, + "twelve scenarios should produce twenty-one /v1/messages requests (total captured: {}, includes count_tokens)", + captured.len() + ); + assert!(messages_only.iter().all(|request| request.stream)); - let scenarios = captured + let scenarios = messages_only .iter() .map(|request| request.scenario.as_str()) .collect::>();