use tauri::{AppHandle, Emitter, State}; use crate::models::{AuthMode, BedrockAuthMethod, Project}; use crate::AppState; /// Build the command to run in the container terminal. /// /// For Bedrock Profile projects, wraps `claude` in a bash script that validates /// the AWS session first. If the SSO session is expired, runs `aws sso login` /// so the user can re-authenticate (the URL is clickable via xterm.js WebLinksAddon). fn build_terminal_cmd(project: &Project, state: &AppState) -> Vec { let is_bedrock_profile = project.auth_mode == AuthMode::Bedrock && project .bedrock_config .as_ref() .map(|b| b.auth_method == BedrockAuthMethod::Profile) .unwrap_or(false); if !is_bedrock_profile { return vec![ "claude".to_string(), "--dangerously-skip-permissions".to_string(), ]; } // Resolve AWS profile: project-level → global settings → "default" let profile = project .bedrock_config .as_ref() .and_then(|b| b.aws_profile.clone()) .or_else(|| state.settings_store.get().global_aws.aws_profile.clone()) .unwrap_or_else(|| "default".to_string()); // Build a bash wrapper that validates credentials, re-auths if needed, // then exec's into claude. let script = format!( r#" echo "Validating AWS session for profile '{profile}'..." if aws sts get-caller-identity --profile '{profile}' >/dev/null 2>&1; then echo "AWS session valid." else echo "AWS session expired or invalid." # Check if this profile uses SSO (has sso_start_url configured) if aws configure get sso_start_url --profile '{profile}' >/dev/null 2>&1; then echo "Starting SSO login — click the URL below to authenticate:" echo "" aws sso login --profile '{profile}' if [ $? -ne 0 ]; then echo "" echo "SSO login failed or was cancelled. Starting Claude anyway..." echo "You may see authentication errors." echo "" fi else echo "Profile '{profile}' does not use SSO. Check your AWS credentials." echo "Starting Claude anyway..." echo "" fi fi exec claude --dangerously-skip-permissions "#, profile = profile ); vec![ "bash".to_string(), "-c".to_string(), script, ] } #[tauri::command] pub async fn open_terminal_session( project_id: String, session_id: String, app_handle: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { let project = state .projects_store .get(&project_id) .ok_or_else(|| format!("Project {} not found", project_id))?; let container_id = project .container_id .as_ref() .ok_or_else(|| "Container not running".to_string())?; let cmd = build_terminal_cmd(&project, &state); let output_event = format!("terminal-output-{}", session_id); let exit_event = format!("terminal-exit-{}", session_id); let app_handle_output = app_handle.clone(); let app_handle_exit = app_handle.clone(); state .exec_manager .create_session( container_id, &session_id, cmd, move |data| { let _ = app_handle_output.emit(&output_event, data); }, Box::new(move || { let _ = app_handle_exit.emit(&exit_event, ()); }), ) .await } #[tauri::command] pub async fn terminal_input( session_id: String, data: Vec, state: State<'_, AppState>, ) -> Result<(), String> { state.exec_manager.send_input(&session_id, data).await } #[tauri::command] pub async fn terminal_resize( session_id: String, cols: u16, rows: u16, state: State<'_, AppState>, ) -> Result<(), String> { state.exec_manager.resize(&session_id, cols, rows).await } #[tauri::command] pub async fn close_terminal_session( session_id: String, state: State<'_, AppState>, ) -> Result<(), String> { state.exec_manager.close_session(&session_id).await; Ok(()) } #[tauri::command] pub async fn paste_image_to_terminal( session_id: String, image_data: Vec, state: State<'_, AppState>, ) -> Result { let container_id = state.exec_manager.get_container_id(&session_id).await?; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis(); let file_name = format!("clipboard_{}.png", timestamp); state .exec_manager .write_file_to_container(&container_id, &file_name, &image_data) .await }