Smooth cursor onboarding (#1143)

* WIP cursor onboarding

Claude rebase fixes

fmt

WIP

WIP

WIP Add login button

Remove auto login check

Login needed detection

Setup script for login and install

Fix install+login case

Update task to inreview upon failure, cleanup

Feedback in next action box

Use errortype isntead of next action to communicate setup needed

* Add auto retry after install

* Fix setup needed detection

* Fix next_run_reason

* Lint and format

* i18n

* Rename to setup helper

* rename ErrorType to NormalizedEntryError

* Better setupHelpText i18n (vibe-kanban a8dd795e)

frontend/src/i18n/locales/en/tasks.json setupHelpText currently isn't very informative. We should change it to 
"{agent_name} isn't setup correctly. Click 'Run Setup' to install it and login." or similar.

* Cursor login should be exempt from the 2 execution process limit (vibe-kanban deca56cd)

frontend/src/components/NormalizedConversation/NextActionCard.tsx

Display run setup even if there are more than 2 execution processes

* Move agent setup to server
This commit is contained in:
Alex Netsch
2025-11-04 11:22:21 +00:00
committed by GitHub
parent 103f55621c
commit 6acc53e581
23 changed files with 486 additions and 166 deletions

View File

@@ -91,6 +91,8 @@ fn generate_types_content() -> String {
executors::actions::coding_agent_initial::CodingAgentInitialRequest::decl(),
executors::actions::coding_agent_follow_up::CodingAgentFollowUpRequest::decl(),
server::routes::task_attempts::CreateTaskAttemptBody::decl(),
server::routes::task_attempts::RunAgentSetupRequest::decl(),
server::routes::task_attempts::RunAgentSetupResponse::decl(),
server::routes::task_attempts::RebaseTaskAttemptRequest::decl(),
server::routes::task_attempts::GitOperationError::decl(),
server::routes::task_attempts::ReplaceProcessRequest::decl(),
@@ -115,6 +117,7 @@ fn generate_types_content() -> String {
executors::logs::FileChange::decl(),
executors::logs::ActionType::decl(),
executors::logs::TodoItem::decl(),
executors::logs::NormalizedEntryError::decl(),
executors::logs::ToolResult::decl(),
executors::logs::ToolResultValueType::decl(),
executors::logs::ToolStatus::decl(),

View File

@@ -1,3 +1,4 @@
pub mod cursor_setup;
pub mod drafts;
pub mod util;
@@ -27,7 +28,8 @@ use executors::{
coding_agent_follow_up::CodingAgentFollowUpRequest,
script::{ScriptContext, ScriptRequest, ScriptRequestLanguage},
},
profile::ExecutorProfileId,
executors::{CodingAgent, ExecutorError},
profile::{ExecutorConfigs, ExecutorProfileId},
};
use git2::BranchType;
use serde::{Deserialize, Serialize};
@@ -141,6 +143,14 @@ impl CreateTaskAttemptBody {
}
}
#[derive(Debug, Deserialize, Serialize, TS)]
pub struct RunAgentSetupRequest {
pub executor_profile_id: ExecutorProfileId,
}
#[derive(Debug, Serialize, TS)]
pub struct RunAgentSetupResponse {}
#[axum::debug_handler]
pub async fn create_task_attempt(
State(deployment): State<DeploymentImpl>,
@@ -194,6 +204,35 @@ pub async fn create_task_attempt(
Ok(ResponseJson(ApiResponse::success(task_attempt)))
}
#[axum::debug_handler]
pub async fn run_agent_setup(
Extension(task_attempt): Extension<TaskAttempt>,
State(deployment): State<DeploymentImpl>,
Json(payload): Json<RunAgentSetupRequest>,
) -> Result<ResponseJson<ApiResponse<RunAgentSetupResponse>>, ApiError> {
let executor_profile_id = payload.executor_profile_id;
let config = ExecutorConfigs::get_cached();
let coding_agent = config.get_coding_agent_or_default(&executor_profile_id);
match coding_agent {
CodingAgent::CursorAgent(_) => {
cursor_setup::run_cursor_setup(&deployment, &task_attempt).await?;
}
_ => return Err(ApiError::Executor(ExecutorError::SetupHelperNotSupported)),
}
deployment
.track_if_analytics_allowed(
"agent_setup_script_executed",
serde_json::json!({
"executor_profile_id": executor_profile_id.to_string(),
"attempt_id": task_attempt.id.to_string(),
}),
)
.await;
Ok(ResponseJson(ApiResponse::success(RunAgentSetupResponse {})))
}
#[derive(Debug, Deserialize, TS)]
pub struct CreateFollowUpAttempt {
pub prompt: String,
@@ -1476,6 +1515,7 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
let task_attempt_id_router = Router::new()
.route("/", get(get_task_attempt))
.route("/follow-up", post(follow_up))
.route("/run-agent-setup", post(run_agent_setup))
.route(
"/draft",
get(drafts::get_draft)

View File

@@ -0,0 +1,99 @@
use db::models::{
execution_process::{ExecutionProcess, ExecutionProcessRunReason},
task_attempt::{TaskAttempt, TaskAttemptError},
};
use deployment::Deployment;
use executors::{
actions::{
ExecutorAction, ExecutorActionType,
script::{ScriptContext, ScriptRequest, ScriptRequestLanguage},
},
executors::cursor::CursorAgent,
};
use services::services::container::ContainerService;
use crate::{error::ApiError, routes::task_attempts::ensure_worktree_path};
pub async fn run_cursor_setup(
deployment: &crate::DeploymentImpl,
task_attempt: &TaskAttempt,
) -> Result<ExecutionProcess, ApiError> {
let latest_process = ExecutionProcess::find_latest_by_task_attempt_and_run_reason(
&deployment.db().pool,
task_attempt.id,
&ExecutionProcessRunReason::CodingAgent,
)
.await?;
let executor_action = if let Some(latest_process) = latest_process {
let latest_action = latest_process
.executor_action()
.map_err(|e| ApiError::TaskAttempt(TaskAttemptError::ValidationError(e.to_string())))?;
get_setup_helper_action()
.await?
.append_action(latest_action.to_owned())
} else {
get_setup_helper_action().await?
};
let _ = ensure_worktree_path(deployment, task_attempt).await?;
let execution_process = deployment
.container()
.start_execution(
task_attempt,
&executor_action,
&ExecutionProcessRunReason::SetupScript,
)
.await?;
Ok(execution_process)
}
async fn get_setup_helper_action() -> Result<ExecutorAction, ApiError> {
#[cfg(not(windows))]
{
let base_command = CursorAgent::base_command();
// First action: Install
let install_script = format!(
r#"#!/bin/bash
set -e
if ! command -v {base_command} &> /dev/null; then
echo "Installing Cursor CLI..."
curl https://cursor.com/install -fsS | bash
echo "Installation complete!"
else
echo "Cursor CLI already installed"
fi
"#
);
let install_request = ScriptRequest {
script: install_script,
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
};
// Second action (chained): Login
let login_script = format!("{base_command} login");
let login_request = ScriptRequest {
script: login_script,
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
};
// Chain them: install → login
Ok(ExecutorAction::new(
ExecutorActionType::ScriptRequest(install_request),
Some(Box::new(ExecutorAction::new(
ExecutorActionType::ScriptRequest(login_request),
None,
))),
))
}
#[cfg(windows)]
{
use executors::executors::ExecutorError::SetupHelperNotSupported;
Err(ApiError::Executor(ExecutorError::SetupHelperNotSupported))
}
}