diff --git a/Cargo.lock b/Cargo.lock index 963d5659..c727296f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -966,7 +966,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1423,7 +1423,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1564,7 +1564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3084,7 +3084,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -3343,7 +3343,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3380,7 +3380,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4449,7 +4449,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2 0.6.2", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -4458,9 +4458,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -4487,9 +4487,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4883,7 +4883,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4954,7 +4954,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5684,7 +5684,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6577,7 +6577,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/slate.json b/slate.json new file mode 100644 index 00000000..a57770c9 --- /dev/null +++ b/slate.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://randomlabs.ai/config.json", + "permission": { + "*": "allow", + "bash": "ask", + "edit": "ask" + } +} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index d46250ec..cbcb2f46 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -690,8 +690,11 @@ pub async fn run_interactive( Some(¤t_input), session.plan_mode, ); - let is_generation = prompts::is_generation_query(¤t_input); let is_planning = session.plan_mode.is_planning(); + // Inherit generation mode for short follow-up messages ("sure", "yes", "go ahead", + // etc.) so the write/shell tool set is not lost between turns. + let is_generation = prompts::is_generation_query(¤t_input) + || (!is_planning && session.last_was_generation && current_input.trim().len() < 60); // Note: using raw_chat_history directly which preserves Reasoning blocks // This is needed for extended thinking to work with multi-turn conversations @@ -1138,6 +1141,10 @@ pub async fn run_interactive( // Add to conversation history with tool call records conversation_history.add_turn(input.clone(), text.clone(), tool_calls.clone()); + // Remember whether this turn had generation tools active so short follow-up + // messages ("sure", "go ahead", etc.) don't lose write/shell access. + session.last_was_generation = is_generation; + // Check if this heavy turn requires immediate compaction // This helps prevent context overflow in subsequent requests if conversation_history.needs_compaction() { diff --git a/src/agent/prompts/mod.rs b/src/agent/prompts/mod.rs index e25e72e6..2a49f658 100644 --- a/src/agent/prompts/mod.rs +++ b/src/agent/prompts/mod.rs @@ -705,6 +705,18 @@ pub fn is_generation_query(query: &str) -> bool { "new feature", "develop", "code", + // Common modification verbs (previously missing) + "fix", + "update", + "add", + "change", + "modify", + "edit", + "configure", + "setup", + "set up", + "patch", + "install", // Plan execution keywords - needed for plan continuation "plan", "continue", diff --git a/src/agent/session/mod.rs b/src/agent/session/mod.rs index 8b60caff..d6bd0b7e 100644 --- a/src/agent/session/mod.rs +++ b/src/agent/session/mod.rs @@ -34,6 +34,10 @@ pub struct ChatSession { pub token_usage: TokenUsage, /// Current planning mode state pub plan_mode: PlanMode, + /// Whether the previous turn used generation mode (write/shell tools active). + /// Used so short follow-up messages ("sure", "go ahead", "yes") inherit the + /// tool set from the previous turn instead of losing write/shell access. + pub last_was_generation: bool, /// Session loaded via /resume command, to be processed by main loop pub pending_resume: Option, /// Platform session state (selected project/org context) @@ -58,6 +62,7 @@ impl ChatSession { history: Vec::new(), token_usage: TokenUsage::new(), plan_mode: PlanMode::default(), + last_was_generation: false, pending_resume: None, platform_session, } diff --git a/src/agent/tools/platform/create_deployment_config.rs b/src/agent/tools/platform/create_deployment_config.rs index 790e44f3..b825152d 100644 --- a/src/agent/tools/platform/create_deployment_config.rs +++ b/src/agent/tools/platform/create_deployment_config.rs @@ -13,13 +13,12 @@ use crate::platform::api::types::{ build_cloud_runner_config_v2, }; use crate::platform::api::{PlatformApiClient, PlatformApiError}; +use crate::platform::session::PlatformSession; use std::str::FromStr; /// Arguments for the create deployment config tool #[derive(Debug, Deserialize)] pub struct CreateDeploymentConfigArgs { - /// The project UUID - pub project_id: String, /// Service name for the deployment pub service_name: String, /// Repository ID from GitHub integration @@ -102,7 +101,6 @@ A deployment config defines how to build and deploy a service, including: - Auto-deploy settings **Required Parameters:** -- project_id: The project UUID - service_name: Name for the service (lowercase, hyphens allowed) - repository_id: GitHub repository ID (from platform GitHub integration) - repository_full_name: Full repo name like "owner/repo" @@ -138,10 +136,6 @@ A deployment config defines how to build and deploy a service, including: parameters: json!({ "type": "object", "properties": { - "project_id": { - "type": "string", - "description": "The UUID of the project" - }, "service_name": { "type": "string", "description": "Name for the service (lowercase, hyphens allowed)" @@ -218,7 +212,7 @@ A deployment config defines how to build and deploy a service, including: } }, "required": [ - "project_id", "service_name", "repository_id", "repository_full_name", + "service_name", "repository_id", "repository_full_name", "port", "branch", "target_type", "provider", "environment_id" ] }), @@ -226,19 +220,30 @@ A deployment config defines how to build and deploy a service, including: } async fn call(&self, args: Self::Args) -> Result { - // Validate required fields - if args.project_id.trim().is_empty() { + // Load project_id from session (authoritative source — prevents stale IDs from LLM context) + let session = match PlatformSession::load() { + Ok(s) => s, + Err(_) => { + return Ok(format_error_for_llm( + "create_deployment_config", + ErrorCategory::InternalError, + "Failed to load platform session", + Some(vec!["Try authenticating with `sync-ctl auth login`"]), + )); + } + }; + + if !session.is_project_selected() { return Ok(format_error_for_llm( "create_deployment_config", ErrorCategory::ValidationFailed, - "project_id cannot be empty", - Some(vec![ - "Use list_projects to find valid project IDs", - "Use current_context to get the selected project", - ]), + "No project selected", + Some(vec!["Use select_project to choose a project first"]), )); } + let project_id = session.project_id.clone().unwrap_or_default(); + if args.service_name.trim().is_empty() { return Ok(format_error_for_llm( "create_deployment_config", @@ -316,7 +321,7 @@ A deployment config defines how to build and deploy a service, including: if let Some(ref provider) = provider_enum { if matches!(provider, CloudProvider::Gcp | CloudProvider::Azure) { if let Ok(credential) = client - .check_provider_connection(provider, &args.project_id) + .check_provider_connection(provider, &project_id) .await { if let Some(cred) = credential { @@ -351,7 +356,7 @@ A deployment config defines how to build and deploy a service, including: // Note: Send both field name variants (dockerfile/dockerfilePath, context/buildContext) // for backend compatibility - different endpoints may expect different field names let request = CreateDeploymentConfigRequest { - project_id: args.project_id.clone(), + project_id, service_name: args.service_name.clone(), repository_id: args.repository_id, repository_full_name: args.repository_full_name.clone(), diff --git a/src/agent/tools/platform/trigger_deployment.rs b/src/agent/tools/platform/trigger_deployment.rs index 0bc138d7..262d8b38 100644 --- a/src/agent/tools/platform/trigger_deployment.rs +++ b/src/agent/tools/platform/trigger_deployment.rs @@ -9,12 +9,11 @@ use serde_json::json; use crate::agent::tools::error::{ErrorCategory, format_error_for_llm}; use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest}; +use crate::platform::session::PlatformSession; /// Arguments for the trigger deployment tool #[derive(Debug, Deserialize)] pub struct TriggerDeploymentArgs { - /// The project ID for the deployment - pub project_id: String, /// The deployment config ID to use pub config_id: String, /// Optional specific commit SHA to deploy @@ -56,13 +55,12 @@ Starts a new deployment for the specified config. Returns a task ID that can be used to monitor deployment progress with `get_deployment_status`. **Parameters:** -- project_id: The project UUID -- config_id: The deployment config ID (get from list_deployment_configs) +- config_id: The deployment config ID (get from list_deployment_configs or create_deployment_config) - commit_sha: Optional specific commit to deploy (defaults to latest on branch) **Prerequisites:** - User must be authenticated via `sync-ctl auth login` -- A deployment config must exist for the project +- A deployment config must exist (use create_deployment_config first if needed) **Use Cases:** - Deploy the latest code from a branch @@ -77,50 +75,57 @@ used to monitor deployment progress with `get_deployment_status`. parameters: json!({ "type": "object", "properties": { - "project_id": { - "type": "string", - "description": "The UUID of the project" - }, "config_id": { "type": "string", - "description": "The deployment config ID (from list_deployment_configs)" + "description": "The deployment config ID (from list_deployment_configs or create_deployment_config)" }, "commit_sha": { "type": "string", "description": "Optional: specific commit SHA to deploy (defaults to latest)" } }, - "required": ["project_id", "config_id"] + "required": ["config_id"] }), } } async fn call(&self, args: Self::Args) -> Result { - // Validate project_id - if args.project_id.trim().is_empty() { + // Validate config_id + if args.config_id.trim().is_empty() { return Ok(format_error_for_llm( "trigger_deployment", ErrorCategory::ValidationFailed, - "project_id cannot be empty", + "config_id cannot be empty", Some(vec![ - "Use list_projects to find valid project IDs", - "Use select_project to set the current project context", + "Use list_deployment_configs to find available deployment configs", ]), )); } - // Validate config_id - if args.config_id.trim().is_empty() { + // Load project_id from session (authoritative source) + let session = match PlatformSession::load() { + Ok(s) => s, + Err(_) => { + return Ok(format_error_for_llm( + "trigger_deployment", + ErrorCategory::InternalError, + "Failed to load platform session", + Some(vec!["Try authenticating with `sync-ctl auth login`"]), + )); + } + }; + + if !session.is_project_selected() { return Ok(format_error_for_llm( "trigger_deployment", ErrorCategory::ValidationFailed, - "config_id cannot be empty", - Some(vec![ - "Use list_deployment_configs to find available deployment configs", - ]), + "No project selected", + Some(vec!["Use select_project to choose a project first"]), )); } + let project_id = session.project_id.clone().unwrap_or_default(); + // Create the API client let client = match PlatformApiClient::new() { Ok(c) => c, @@ -131,7 +136,7 @@ used to monitor deployment progress with `get_deployment_status`. // Build the request let request = TriggerDeploymentRequest { - project_id: args.project_id.clone(), + project_id, config_id: args.config_id.clone(), commit_sha: args.commit_sha.clone(), }; diff --git a/src/platform/api/client.rs b/src/platform/api/client.rs index 6d09fc73..99b560c0 100644 --- a/src/platform/api/client.rs +++ b/src/platform/api/client.rs @@ -722,7 +722,7 @@ impl PlatformApiClient { request: &TriggerDeploymentRequest, ) -> Result { log::debug!( - "Triggering deployment: POST /api/deployment-configs/deploy with projectId={}, configId={}", + "Triggering deployment: POST /api/deployment-configs/deploy with projectId={} configId={}", request.project_id, request.config_id ); diff --git a/src/platform/api/types.rs b/src/platform/api/types.rs index 32f03f96..64d2cb95 100644 --- a/src/platform/api/types.rs +++ b/src/platform/api/types.rs @@ -335,7 +335,7 @@ pub struct CreateDeploymentConfigResponse { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TriggerDeploymentRequest { - /// Project ID for the deployment + /// The project UUID this config belongs to pub project_id: String, /// Deployment config ID to use pub config_id: String, diff --git a/src/wizard/orchestrator.rs b/src/wizard/orchestrator.rs index f551259d..1a558ee5 100644 --- a/src/wizard/orchestrator.rs +++ b/src/wizard/orchestrator.rs @@ -524,11 +524,7 @@ pub async fn run_wizard( }; // Debug: Show trigger request - log::debug!( - "Trigger request: projectId={}, configId={}", - trigger_request.project_id, - trigger_request.config_id - ); + log::debug!("Trigger request: configId={}", trigger_request.config_id); match client.trigger_deployment(&trigger_request).await { Ok(response) => {