diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index accd165b..82b51656 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -82,6 +82,9 @@ fn generate_types_content() -> String { server::routes::task_attempts::ChangeTargetBranchResponse::decl(), server::routes::task_attempts::RenameBranchRequest::decl(), server::routes::task_attempts::RenameBranchResponse::decl(), + server::routes::task_attempts::CommitCompareResult::decl(), + server::routes::task_attempts::OpenEditorRequest::decl(), + server::routes::task_attempts::OpenEditorResponse::decl(), server::routes::shared_tasks::AssignSharedTaskRequest::decl(), server::routes::shared_tasks::AssignSharedTaskResponse::decl(), server::routes::tasks::ShareTaskResponse::decl(), @@ -101,7 +104,6 @@ fn generate_types_content() -> String { services::services::git::GitBranch::decl(), utils::diff::Diff::decl(), utils::diff::DiffChangeKind::decl(), - services::services::github::RepositoryInfo::decl(), executors::command::CommandBuilder::decl(), executors::profile::ExecutorProfileId::decl(), executors::profile::ExecutorConfig::decl(), @@ -130,7 +132,6 @@ fn generate_types_content() -> String { server::routes::task_attempts::gh_cli_setup::GhCliSetupError::decl(), server::routes::task_attempts::RebaseTaskAttemptRequest::decl(), server::routes::task_attempts::GitOperationError::decl(), - server::routes::task_attempts::ReplaceProcessRequest::decl(), server::routes::task_attempts::CommitInfo::decl(), server::routes::task_attempts::BranchStatus::decl(), services::services::git::ConflictOp::decl(), diff --git a/crates/server/src/routes/execution_processes.rs b/crates/server/src/routes/execution_processes.rs index c40c1fda..b5c00c00 100644 --- a/crates/server/src/routes/execution_processes.rs +++ b/crates/server/src/routes/execution_processes.rs @@ -29,21 +29,6 @@ pub struct ExecutionProcessQuery { pub show_soft_deleted: Option, } -pub async fn get_execution_processes( - State(deployment): State, - Query(query): Query, -) -> Result>>, ApiError> { - let pool = &deployment.db().pool; - let execution_processes = ExecutionProcess::find_by_task_attempt_id( - pool, - query.task_attempt_id, - query.show_soft_deleted.unwrap_or(false), - ) - .await?; - - Ok(ResponseJson(ApiResponse::success(execution_processes))) -} - pub async fn get_execution_process_by_id( Extension(execution_process): Extension, State(_deployment): State, @@ -259,7 +244,6 @@ pub fn router(deployment: &DeploymentImpl) -> Router { )); let task_attempts_router = Router::new() - .route("/", get(get_execution_processes)) .route("/stream/ws", get(stream_execution_processes_ws)) .nest("/{id}", task_attempt_id_router); diff --git a/crates/server/src/routes/tags.rs b/crates/server/src/routes/tags.rs index dfdcdb56..8672c6f9 100644 --- a/crates/server/src/routes/tags.rs +++ b/crates/server/src/routes/tags.rs @@ -3,7 +3,7 @@ use axum::{ extract::{Query, State}, middleware::from_fn_with_state, response::Json as ResponseJson, - routing::get, + routing::{get, put}, }; use db::models::tag::{CreateTag, Tag, UpdateTag}; use deployment::Deployment; @@ -34,12 +34,6 @@ pub async fn get_tags( Ok(ResponseJson(ApiResponse::success(tags))) } -pub async fn get_tag( - Extension(tag): Extension, -) -> Result>, ApiError> { - Ok(Json(ApiResponse::success(tag))) -} - pub async fn create_tag( State(deployment): State, Json(payload): Json, @@ -93,7 +87,7 @@ pub async fn delete_tag( pub fn router(deployment: &DeploymentImpl) -> Router { let tag_router = Router::new() - .route("/", get(get_tag).put(update_tag).delete(delete_tag)) + .route("/", put(update_tag).delete(delete_tag)) .layer(from_fn_with_state(deployment.clone(), load_tag_middleware)); let inner = Router::new() diff --git a/crates/server/src/routes/task_attempts.rs b/crates/server/src/routes/task_attempts.rs index 630b46bb..37729f35 100644 --- a/crates/server/src/routes/task_attempts.rs +++ b/crates/server/src/routes/task_attempts.rs @@ -68,29 +68,6 @@ pub enum GitOperationError { RebaseInProgress, } -#[derive(Debug, Deserialize, Serialize, TS)] -pub struct ReplaceProcessRequest { - /// Process to replace (delete this and later ones) - pub process_id: Uuid, - /// New prompt to use for the replacement follow-up - pub prompt: String, - /// Optional variant override - pub variant: Option, - /// If true, allow resetting Git even when uncommitted changes exist - pub force_when_dirty: Option, - /// If false, skip performing the Git reset step (history drop still applies) - pub perform_git_reset: Option, -} - -#[derive(Debug, Serialize, TS)] -pub struct ReplaceProcessResult { - pub deleted_count: i64, - pub git_reset_needed: bool, - pub git_reset_applied: bool, - pub target_before_oid: Option, - pub new_execution_id: Option, -} - #[derive(Debug, Deserialize, Serialize, TS)] pub struct CreateGitHubPrRequest { pub title: String, @@ -98,13 +75,6 @@ pub struct CreateGitHubPrRequest { pub target_branch: Option, } -#[derive(Debug, Serialize)] -pub struct FollowUpResponse { - pub message: String, - pub actual_attempt_id: Uuid, - pub created_new_attempt: bool, -} - #[derive(Debug, Deserialize)] pub struct TaskAttemptQuery { pub task_id: Option, @@ -394,135 +364,6 @@ pub async fn follow_up( Ok(ResponseJson(ApiResponse::success(execution_process))) } -#[axum::debug_handler] -pub async fn replace_process( - Extension(task_attempt): Extension, - State(deployment): State, - Json(payload): Json, -) -> Result>, ApiError> { - let pool = &deployment.db().pool; - let proc_id = payload.process_id; - let force_when_dirty = payload.force_when_dirty.unwrap_or(false); - let perform_git_reset = payload.perform_git_reset.unwrap_or(true); - - // Validate process belongs to attempt - let process = - ExecutionProcess::find_by_id(pool, proc_id) - .await? - .ok_or(ApiError::TaskAttempt(TaskAttemptError::ValidationError( - "Process not found".to_string(), - )))?; - if process.task_attempt_id != task_attempt.id { - return Err(ApiError::TaskAttempt(TaskAttemptError::ValidationError( - "Process does not belong to this attempt".to_string(), - ))); - } - - // Determine target reset OID: before the target process - let mut target_before_oid = process.before_head_commit.clone(); - if target_before_oid.is_none() { - // Fallback: previous process's after_head_commit - target_before_oid = - ExecutionProcess::find_prev_after_head_commit(pool, task_attempt.id, proc_id).await?; - } - - // Decide if Git reset is needed and apply it - let mut git_reset_needed = false; - let mut git_reset_applied = false; - if let Some(target_oid) = &target_before_oid { - let wt_buf = ensure_worktree_path(&deployment, &task_attempt).await?; - let wt = wt_buf.as_path(); - let is_dirty = deployment - .container() - .is_container_clean(&task_attempt) - .await - .map(|is_clean| !is_clean) - .unwrap_or(false); - - let outcome = deployment.git().reconcile_worktree_to_commit( - wt, - target_oid, - WorktreeResetOptions::new(perform_git_reset, force_when_dirty, is_dirty, false), - ); - git_reset_needed = outcome.needed; - git_reset_applied = outcome.applied; - } - - // Stop any running processes for this attempt - deployment.container().try_stop(&task_attempt).await; - - // Soft-drop the target process and all later processes - let deleted_count = ExecutionProcess::drop_at_and_after(pool, task_attempt.id, proc_id).await?; - - // Build follow-up executor action using the original process profile - let initial_executor_profile_id = match &process - .executor_action() - .map_err(|e| ApiError::TaskAttempt(TaskAttemptError::ValidationError(e.to_string())))? - .typ - { - ExecutorActionType::CodingAgentInitialRequest(request) => { - Ok(request.executor_profile_id.clone()) - } - ExecutorActionType::CodingAgentFollowUpRequest(request) => { - Ok(request.executor_profile_id.clone()) - } - _ => Err(ApiError::TaskAttempt(TaskAttemptError::ValidationError( - "Couldn't find profile from executor action".to_string(), - ))), - }?; - - let executor_profile_id = ExecutorProfileId { - executor: initial_executor_profile_id.executor, - variant: payload - .variant - .or(initial_executor_profile_id.variant.clone()), - }; - - // Use latest session_id from remaining (earlier) processes; if none exists, start a fresh initial request - let latest_session_id = - ExecutionProcess::find_latest_session_id_by_task_attempt(pool, task_attempt.id).await?; - - let action = if let Some(session_id) = latest_session_id { - let follow_up_request = CodingAgentFollowUpRequest { - prompt: payload.prompt.clone(), - session_id, - executor_profile_id, - }; - ExecutorAction::new( - ExecutorActionType::CodingAgentFollowUpRequest(follow_up_request), - None, - ) - } else { - // No prior session (e.g., replacing the first run) → start a fresh initial request - ExecutorAction::new( - ExecutorActionType::CodingAgentInitialRequest( - executors::actions::coding_agent_initial::CodingAgentInitialRequest { - prompt: payload.prompt.clone(), - executor_profile_id, - }, - ), - None, - ) - }; - - let execution_process = deployment - .container() - .start_execution( - &task_attempt, - &action, - &ExecutionProcessRunReason::CodingAgent, - ) - .await?; - - Ok(ResponseJson(ApiResponse::success(ReplaceProcessResult { - deleted_count, - git_reset_needed, - git_reset_applied, - target_before_oid, - new_execution_id: Some(execution_process.id), - }))) -} - #[axum::debug_handler] pub async fn stream_task_attempt_diff_ws( ws: WebSocketUpgrade, @@ -893,7 +734,7 @@ pub async fn create_github_pr( } } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, TS)] pub struct OpenEditorRequest { editor_type: Option, file_path: Option, @@ -907,14 +748,14 @@ pub struct OpenEditorResponse { pub async fn open_task_attempt_in_editor( Extension(task_attempt): Extension, State(deployment): State, - Json(payload): Json>, + Json(payload): Json, ) -> Result>, ApiError> { // Get the task attempt to access the worktree path let base_path_buf = ensure_worktree_path(&deployment, &task_attempt).await?; let base_path = base_path_buf.as_path(); // If a specific file path is provided, use it; otherwise use the base path - let path = if let Some(file_path) = payload.as_ref().and_then(|req| req.file_path.as_ref()) { + let path = if let Some(file_path) = payload.file_path.as_ref() { base_path.join(file_path) } else { base_path.to_path_buf() @@ -922,7 +763,7 @@ pub async fn open_task_attempt_in_editor( let editor_config = { let config = deployment.config().read().await; - let editor_type_str = payload.as_ref().and_then(|req| req.editor_type.as_deref()); + let editor_type_str = payload.editor_type.as_deref(); config.editor.with_override(editor_type_str) }; @@ -940,7 +781,7 @@ pub async fn open_task_attempt_in_editor( "task_attempt_editor_opened", serde_json::json!({ "attempt_id": task_attempt.id.to_string(), - "editor_type": payload.as_ref().and_then(|req| req.editor_type.as_ref()), + "editor_type": payload.editor_type.as_ref(), "remote_mode": url.is_some(), }), ) @@ -1375,40 +1216,6 @@ pub async fn abort_conflicts_task_attempt( Ok(ResponseJson(ApiResponse::success(()))) } -#[derive(serde::Deserialize)] -pub struct DeleteFileQuery { - file_path: String, -} - -#[axum::debug_handler] -pub async fn delete_task_attempt_file( - Extension(task_attempt): Extension, - Query(query): Query, - State(deployment): State, -) -> Result>, ApiError> { - let container_ref = deployment - .container() - .ensure_container_exists(&task_attempt) - .await?; - let worktree_path = std::path::Path::new(&container_ref); - - // Use GitService to delete file and commit - let _commit_id = deployment - .git() - .delete_file_and_commit(worktree_path, &query.file_path) - .map_err(|e| { - tracing::error!( - "Failed to delete file '{}' from task attempt {}: {}", - query.file_path, - task_attempt.id, - e - ); - ApiError::GitService(e) - })?; - - Ok(ResponseJson(ApiResponse::success(()))) -} - #[axum::debug_handler] pub async fn start_dev_server( Extension(task_attempt): Extension, @@ -1701,7 +1508,6 @@ pub fn router(deployment: &DeploymentImpl) -> Router { .delete(drafts::delete_draft), ) .route("/draft/queue", post(drafts::set_draft_queue)) - .route("/replace-process", post(replace_process)) .route("/commit-info", get(get_commit_info)) .route("/commit-compare", get(compare_commit_to_head)) .route("/start-dev-server", post(start_dev_server)) @@ -1714,7 +1520,6 @@ pub fn router(deployment: &DeploymentImpl) -> Router { .route("/pr", post(create_github_pr)) .route("/pr/attach", post(attach_existing_pr)) .route("/open-editor", post(open_task_attempt_in_editor)) - .route("/delete-file", post(delete_task_attempt_file)) .route("/children", get(get_task_attempt_children)) .route("/stop", post(stop_task_attempt_execution)) .route("/change-target-branch", post(change_target_branch)) diff --git a/crates/services/src/services/git.rs b/crates/services/src/services/git.rs index 50d8f0c7..faaa25f9 100644 --- a/crates/services/src/services/git.rs +++ b/crates/services/src/services/git.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::Path}; use chrono::{DateTime, Utc}; use git2::{ BranchType, Delta, DiffFindOptions, DiffOptions, Error as GitError, Reference, Remote, - Repository, Sort, build::CheckoutBuilder, + Repository, Sort, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -1077,16 +1077,6 @@ impl GitService { Ok((st.uncommitted_tracked, st.untracked)) } - /// Expose full worktree status details (CLI porcelain parsing) - pub fn get_worktree_status( - &self, - worktree_path: &Path, - ) -> Result { - let cli = GitCli::new(); - cli.get_worktree_status(worktree_path) - .map_err(|e| GitServiceError::InvalidRepository(format!("git status failed: {e}"))) - } - /// Evaluate whether any action is needed to reset to `target_commit_oid` and /// optionally perform the actions. pub fn reconcile_worktree_to_commit( @@ -1151,33 +1141,6 @@ impl GitService { Ok(()) } - /// Create a local branch at the current HEAD - pub fn create_branch( - &self, - repo_path: &Path, - branch_name: &str, - ) -> Result<(), GitServiceError> { - let repo = self.open_repo(repo_path)?; - let head_commit = repo.head()?.peel_to_commit()?; - repo.branch(branch_name, &head_commit, true)?; - Ok(()) - } - - /// Checkout a local branch in the given working tree - pub fn checkout_branch( - &self, - repo_path: &Path, - branch_name: &str, - ) -> Result<(), GitServiceError> { - let repo = self.open_repo(repo_path)?; - let refname = format!("refs/heads/{branch_name}"); - repo.set_head(&refname)?; - let mut co = CheckoutBuilder::new(); - co.force(); - repo.checkout_head(Some(&mut co))?; - Ok(()) - } - /// Add a worktree for a branch, optionally creating the branch pub fn add_worktree( &self, @@ -1212,23 +1175,6 @@ impl GitService { Ok(()) } - /// Set or add a remote URL - pub fn set_remote( - &self, - repo_path: &Path, - name: &str, - url: &str, - ) -> Result<(), GitServiceError> { - let repo = self.open_repo(repo_path)?; - match repo.find_remote(name) { - Ok(_) => repo.remote_set_url(name, url)?, - Err(_) => { - repo.remote(name, url)?; - } - } - Ok(()) - } - pub fn get_all_branches(&self, repo_path: &Path) -> Result, git2::Error> { let repo = Repository::open(repo_path)?; let current_branch = self.get_current_branch(repo_path).unwrap_or_default(); @@ -1588,69 +1534,6 @@ impl GitService { } } - /// Delete a file from the repository and commit the change - pub fn delete_file_and_commit( - &self, - worktree_path: &Path, - file_path: &str, - ) -> Result { - let repo = Repository::open(worktree_path)?; - - // Get the absolute path to the file within the worktree - let file_full_path = worktree_path.join(file_path); - - // Check if file exists and delete it - if file_full_path.exists() { - std::fs::remove_file(&file_full_path).map_err(|e| { - GitServiceError::IoError(std::io::Error::other(format!( - "Failed to delete file {file_path}: {e}" - ))) - })?; - } - - // Stage the deletion - let mut index = repo.index()?; - index.remove_path(Path::new(file_path))?; - index.write()?; - - // Create a commit for the file deletion - let signature = self.signature_with_fallback(&repo)?; - let tree_id = index.write_tree()?; - let tree = repo.find_tree(tree_id)?; - - // Get the current HEAD commit - let head = repo.head()?; - let parent_commit = head.peel_to_commit()?; - - let commit_message = format!("Delete file: {file_path}"); - let commit_id = repo.commit( - Some("HEAD"), - &signature, - &signature, - &commit_message, - &tree, - &[&parent_commit], - )?; - - Ok(commit_id.to_string()) - } - - /// Get the default branch name for the repository - pub fn get_default_branch_name(&self, repo_path: &Path) -> Result { - let repo = self.open_repo(repo_path)?; - - match repo.head() { - Ok(head_ref) => Ok(head_ref.shorthand().unwrap_or("main").to_string()), - Err(e) - if e.class() == git2::ErrorClass::Reference - && e.code() == git2::ErrorCode::UnbornBranch => - { - Ok("main".to_string()) // Repository has no commits yet - } - Err(_) => Ok("main".to_string()), // Fallback - } - } - /// Extract GitHub owner and repo name from git repo path pub fn get_github_repo_info( &self, diff --git a/crates/services/src/services/github.rs b/crates/services/src/services/github.rs index 2fcaf2a5..d6ac6d7e 100644 --- a/crates/services/src/services/github.rs +++ b/crates/services/src/services/github.rs @@ -25,9 +25,6 @@ pub enum GitHubServiceError { #[ts(skip)] #[error("Pull request error: {0}")] PullRequest(String), - #[ts(skip)] - #[error("Branch error: {0}")] - Branch(String), #[error("GitHub token is invalid or expired.")] TokenInvalid, #[error("Insufficient permissions")] @@ -153,19 +150,6 @@ pub struct GitHubService { gh_cli: GhCli, } -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -pub struct RepositoryInfo { - pub id: i64, - pub name: String, - pub full_name: String, - pub owner: String, - pub description: Option, - pub clone_url: String, - pub ssh_url: String, - pub default_branch: String, - pub private: bool, -} - impl GitHubService { /// Create a new GitHub service with authentication pub fn new() -> Result { @@ -220,26 +204,6 @@ impl GitHubService { .await } - pub async fn fetch_repository_id( - &self, - owner: &str, - repo: &str, - ) -> Result { - let owner = owner.to_string(); - let repo = repo.to_string(); - let cli = self.gh_cli.clone(); - let owner_for_cli = owner.clone(); - let repo_for_cli = repo.clone(); - task::spawn_blocking(move || cli.repo_database_id(&owner_for_cli, &repo_for_cli)) - .await - .map_err(|err| { - GitHubServiceError::Repository(format!( - "Failed to execute GitHub CLI for repo lookup: {err}" - )) - })? - .map_err(GitHubServiceError::from) - } - async fn create_pr_via_cli( &self, repo_info: &GitHubRepoInfo, @@ -350,14 +314,4 @@ impl GitHubService { }) .await } - - #[cfg(feature = "cloud")] - pub async fn list_repositories( - &self, - _page: u8, - ) -> Result, GitHubServiceError> { - Err(GitHubServiceError::Repository( - "Listing repositories via GitHub CLI is not supported.".into(), - )) - } } diff --git a/crates/services/src/services/github/cli.rs b/crates/services/src/services/github/cli.rs index caa58d97..6f9fdc5d 100644 --- a/crates/services/src/services/github/cli.rs +++ b/crates/services/src/services/github/cli.rs @@ -41,9 +41,8 @@ impl GhCli { /// Ensure the GitHub CLI binary is discoverable. fn ensure_available(&self) -> Result<(), GhCliError> { - resolve_executable_path_blocking("gh") - .ok_or(GhCliError::NotAvailable) - .map(|_| ()) + resolve_executable_path_blocking("gh").ok_or(GhCliError::NotAvailable)?; + Ok(()) } /// Generic helper to execute `gh ` and return stdout on success. @@ -128,21 +127,6 @@ impl GhCli { } } - /// Fetch repository numeric ID via `gh api`. - pub fn repo_database_id(&self, owner: &str, repo: &str) -> Result { - let raw = self.run(["api", &format!("repos/{owner}/{repo}"), "--method", "GET"])?; - let value: Value = serde_json::from_str(raw.trim()).map_err(|err| { - GhCliError::UnexpectedOutput(format!( - "Failed to parse gh api repos response: {err}; raw: {raw}" - )) - })?; - value.get("id").and_then(Value::as_i64).ok_or_else(|| { - GhCliError::UnexpectedOutput(format!( - "gh api repos response missing numeric repository id: {value:#?}" - )) - }) - } - /// Retrieve details for a single pull request. pub fn view_pr( &self, diff --git a/crates/services/tests/git_ops_safety.rs b/crates/services/tests/git_ops_safety.rs index e9ff5ebd..c112a63c 100644 --- a/crates/services/tests/git_ops_safety.rs +++ b/crates/services/tests/git_ops_safety.rs @@ -557,7 +557,8 @@ fn merge_refuses_with_staged_changes_on_base() { let (repo_path, worktree_path) = setup_repo_with_worktree(&td); let s = GitService::new(); // ensure main is checked out - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + checkout_branch(&repo, "main"); // feature adds change and commits write_file(&worktree_path, "m.txt", "feature\n"); let wt_repo = Repository::open(&worktree_path).unwrap(); @@ -577,7 +578,8 @@ fn merge_preserves_unstaged_changes_on_base() { let td = TempDir::new().unwrap(); let (repo_path, worktree_path) = setup_repo_with_worktree(&td); let s = GitService::new(); - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + checkout_branch(&repo, "main"); // modify unstaged write_file(&repo_path, "common.txt", "local edited\n"); // feature modifies a different file @@ -601,8 +603,9 @@ fn update_ref_does_not_destroy_feature_worktree_dirty_state() { let td = TempDir::new().unwrap(); let (repo_path, worktree_path) = setup_repo_with_worktree(&td); let s = GitService::new(); + let repo = Repository::open(&repo_path).unwrap(); // ensure main is checked out - s.checkout_branch(&repo_path, "main").unwrap(); + checkout_branch(&repo, "main"); // feature makes an initial change and commits write_file(&worktree_path, "f.txt", "feat\n"); let wt_repo = Repository::open(&worktree_path).unwrap(); @@ -915,17 +918,15 @@ fn merge_refreshes_main_worktree_when_on_base() { let repo_path = td.path().join("repo_refresh"); let s = GitService::new(); s.initialize_repo_with_main_branch(&repo_path).unwrap(); - { - let repo = Repository::open(&repo_path).unwrap(); - configure_user(&repo); - } - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + configure_user(&repo); + checkout_branch(&repo, "main"); // Baseline file write_file(&repo_path, "file.txt", "base\n"); let _ = s.commit(&repo_path, "add base").unwrap(); // Create feature branch and worktree - s.create_branch(&repo_path, "feature").unwrap(); + create_branch_from_head(&repo, "feature"); let wt = td.path().join("wt_refresh"); s.add_worktree(&repo_path, &wt, "feature", false).unwrap(); // Modify file in worktree and commit @@ -950,11 +951,9 @@ fn sparse_checkout_respected_in_worktree_diffs_and_commit() { let repo_path = td.path().join("repo_sparse"); let s = GitService::new(); s.initialize_repo_with_main_branch(&repo_path).unwrap(); - { - let repo = Repository::open(&repo_path).unwrap(); - configure_user(&repo); - } - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + configure_user(&repo); + checkout_branch(&repo, "main"); // baseline content write_file(&repo_path, "included/a.txt", "A\n"); write_file(&repo_path, "excluded/b.txt", "B\n"); @@ -968,7 +967,7 @@ fn sparse_checkout_respected_in_worktree_diffs_and_commit() { .unwrap(); // create feature branch and worktree - s.create_branch(&repo_path, "feature").unwrap(); + create_branch_from_head(&repo, "feature"); let wt = td.path().join("wt_sparse"); s.add_worktree(&repo_path, &wt, "feature", false).unwrap(); @@ -1032,16 +1031,14 @@ fn worktree_diff_ignores_commits_where_base_branch_is_ahead() { let repo_path = td.path().join("repo_base_ahead"); let s = GitService::new(); s.initialize_repo_with_main_branch(&repo_path).unwrap(); - { - let repo = Repository::open(&repo_path).unwrap(); - configure_user(&repo); - } - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + configure_user(&repo); + checkout_branch(&repo, "main"); write_file(&repo_path, "shared.txt", "base\n"); let _ = s.commit(&repo_path, "add shared").unwrap(); - s.create_branch(&repo_path, "feature").unwrap(); + create_branch_from_head(&repo, "feature"); let wt = td.path().join("wt_base_ahead"); s.add_worktree(&repo_path, &wt, "feature", false).unwrap(); @@ -1077,11 +1074,9 @@ fn init_repo_only_service(root: &TempDir) -> PathBuf { let repo_path = root.path().join("repo_svc"); let s = GitService::new(); s.initialize_repo_with_main_branch(&repo_path).unwrap(); - { - let repo = Repository::open(&repo_path).unwrap(); - configure_user(&repo); - } - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + configure_user(&repo); + checkout_branch(&repo, "main"); repo_path } @@ -1089,11 +1084,12 @@ fn init_repo_only_service(root: &TempDir) -> PathBuf { fn merge_binary_conflict_does_not_move_ref() { let td = TempDir::new().unwrap(); let repo_path = init_repo_only_service(&td); + let repo = Repository::open(&repo_path).unwrap(); let s = GitService::new(); // seed let _ = s.commit(&repo_path, "seed").unwrap(); // create feature branch and worktree - s.create_branch(&repo_path, "feature").unwrap(); + create_branch_from_head(&repo, "feature"); let worktree_path = td.path().join("wt_bin"); s.add_worktree(&repo_path, &worktree_path, "feature", false) .unwrap(); @@ -1119,11 +1115,12 @@ fn merge_binary_conflict_does_not_move_ref() { fn merge_rename_vs_modify_conflict_does_not_move_ref() { let td = TempDir::new().unwrap(); let repo_path = init_repo_only_service(&td); + let repo = Repository::open(&repo_path).unwrap(); let s = GitService::new(); // base file fs::write(repo_path.join("conflict.txt"), b"base\n").unwrap(); let _ = s.commit(&repo_path, "base").unwrap(); - s.create_branch(&repo_path, "feature").unwrap(); + create_branch_from_head(&repo, "feature"); let worktree_path = td.path().join("wt_ren"); s.add_worktree(&repo_path, &worktree_path, "feature", false) .unwrap(); @@ -1185,7 +1182,8 @@ fn merge_leaves_no_staged_changes_on_target_branch() { // Ensure main repo is on the base branch (triggers CLI merge path) let s = GitService::new(); - s.checkout_branch(&repo_path, "main").unwrap(); + let repo = Repository::open(&repo_path).unwrap(); + checkout_branch(&repo, "main"); // Feature branch makes some changes write_file(&worktree_path, "feature_file.txt", "feature content\n"); @@ -1236,7 +1234,6 @@ fn worktree_to_worktree_merge_leaves_no_staged_changes() { .expect("init repo"); let repo = Repository::open(&repo_path).unwrap(); configure_user(&repo); - checkout_branch(&repo, "main"); write_file(&repo_path, "base.txt", "base content\n"); commit_all(&repo, "initial commit"); @@ -1308,7 +1305,7 @@ fn merge_into_orphaned_branch_uses_libgit2_fallback() { .unwrap(); // Ensure main repo is on different branch and no worktree has orphaned-feature - service.checkout_branch(&repo_path, "main").unwrap(); + checkout_branch(&repo, "main"); // Make changes in source worktree write_file( @@ -1369,7 +1366,6 @@ fn merge_base_ahead_of_task_should_error() { .expect("init repo"); let repo = Repository::open(&repo_path).unwrap(); configure_user(&repo); - checkout_branch(&repo, "main"); // Initial commit on main write_file(&repo_path, "base.txt", "initial content\n"); diff --git a/crates/services/tests/git_workflow.rs b/crates/services/tests/git_workflow.rs index dd4884c4..fb14ba3a 100644 --- a/crates/services/tests/git_workflow.rs +++ b/crates/services/tests/git_workflow.rs @@ -4,6 +4,7 @@ use std::{ path::{Path, PathBuf}, }; +use git2::{Repository, build::CheckoutBuilder}; use services::services::{ git::{DiffTarget, GitCli, GitService}, github::{GitHubRepoInfo, GitHubServiceError}, @@ -60,10 +61,24 @@ fn init_repo_main(root: &TempDir) -> PathBuf { let s = GitService::new(); s.initialize_repo_with_main_branch(&path).unwrap(); configure_user(&path, "Test User", "test@example.com"); - s.checkout_branch(&path, "main").unwrap(); + checkout_branch(&path, "main"); path } +fn checkout_branch(repo_path: &Path, name: &str) { + let repo = Repository::open(repo_path).unwrap(); + repo.set_head(&format!("refs/heads/{name}")).unwrap(); + let mut co = CheckoutBuilder::new(); + co.force(); + repo.checkout_head(Some(&mut co)).unwrap(); +} + +fn create_branch(repo_path: &Path, name: &str) { + let repo = Repository::open(repo_path).unwrap(); + let head = repo.head().unwrap().peel_to_commit().unwrap(); + let _ = repo.branch(name, &head, true).unwrap(); +} + #[test] fn commit_empty_message_behaviour() { let td = TempDir::new().unwrap(); @@ -148,35 +163,6 @@ fn staged_but_uncommitted_changes_is_dirty() { assert!(!s.is_worktree_clean(&repo_path).unwrap()); } -#[test] -fn delete_nonexistent_file_creates_noop_commit() { - let td = TempDir::new().unwrap(); - let repo_path = init_repo_main(&td); - // baseline commit first so we have HEAD - write_file(&repo_path, "seed.txt", "s\n"); - let s = GitService::new(); - let _ = s.commit(&repo_path, "seed").unwrap(); - let before = s.get_head_info(&repo_path).unwrap().oid; - let res = s.delete_file_and_commit(&repo_path, "nope.txt").unwrap(); - let after = s.get_head_info(&repo_path).unwrap().oid; - assert_ne!(before, after); - assert_eq!(after, res); -} - -#[test] -fn delete_directory_path_errors() { - let td = TempDir::new().unwrap(); - let repo_path = init_repo_main(&td); - // create and commit a file so repo has history - write_file(&repo_path, "dir/file.txt", "z\n"); - let s = GitService::new(); - let _ = s.commit(&repo_path, "add file").unwrap(); - // directory path should cause an error - let s = GitService::new(); - let res = s.delete_file_and_commit(&repo_path, "dir"); - assert!(res.is_err()); -} - #[test] fn worktree_clean_detects_staged_deleted_and_renamed() { let td = TempDir::new().unwrap(); @@ -206,8 +192,8 @@ fn diff_added_binary_file_has_no_content() { let s = GitService::new(); let _ = s.commit(&repo_path, "base").unwrap(); // branch with binary file - s.create_branch(&repo_path, "feature").unwrap(); - s.checkout_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); + checkout_branch(&repo_path, "feature"); // write binary with null byte let mut f = fs::File::create(repo_path.join("bin.dat")).unwrap(); f.write_all(&[0u8, 1, 2, 3]).unwrap(); @@ -236,11 +222,7 @@ fn initialize_and_default_branch_and_head_info() { let td = TempDir::new().unwrap(); let repo_path = init_repo_main(&td); - // Default branch should be main let s = GitService::new(); - let def = s.get_default_branch_name(&repo_path).unwrap(); - assert_eq!(def, "main"); - // Head info branch should be main let head = s.get_head_info(&repo_path).unwrap(); assert_eq!(head.branch, "main"); @@ -306,14 +288,14 @@ fn branch_status_ahead_and_behind() { let _ = s.commit(&repo_path, "base").unwrap(); // create feature from main - s.create_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); // advance feature by 1 - s.checkout_branch(&repo_path, "feature").unwrap(); + checkout_branch(&repo_path, "feature"); write_file(&repo_path, "feature.txt", "f1\n"); let _ = s.commit(&repo_path, "f1").unwrap(); // advance main by 1 - s.checkout_branch(&repo_path, "main").unwrap(); + checkout_branch(&repo_path, "main"); write_file(&repo_path, "main.txt", "m1\n"); let _ = s.commit(&repo_path, "m1").unwrap(); @@ -322,7 +304,7 @@ fn branch_status_ahead_and_behind() { assert_eq!((ahead, behind), (1, 1)); // advance feature by one more (ahead 2, behind 1) - s.checkout_branch(&repo_path, "feature").unwrap(); + checkout_branch(&repo_path, "feature"); write_file(&repo_path, "feature2.txt", "f2\n"); let _ = s.commit(&repo_path, "f2").unwrap(); let (ahead2, behind2) = s.get_branch_status(&repo_path, "feature", "main").unwrap(); @@ -333,8 +315,7 @@ fn branch_status_ahead_and_behind() { fn get_all_branches_lists_current_and_others() { let td = TempDir::new().unwrap(); let repo_path = init_repo_main(&td); - let s = GitService::new(); - s.create_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); let s = GitService::new(); let branches = s.get_all_branches(&repo_path).unwrap(); @@ -346,36 +327,6 @@ fn get_all_branches_lists_current_and_others() { assert!(main_entry.is_current); } -#[test] -fn delete_file_and_commit_creates_new_commit() { - let td = TempDir::new().unwrap(); - let repo_path = init_repo_main(&td); - write_file(&repo_path, "to_delete.txt", "bye\n"); - let s = GitService::new(); - let _ = s.commit(&repo_path, "add to_delete").unwrap(); - let before = s.get_head_info(&repo_path).unwrap().oid; - - let new_commit = s - .delete_file_and_commit(&repo_path, "to_delete.txt") - .unwrap(); - let after = s.get_head_info(&repo_path).unwrap().oid; - assert_ne!(before, after); - assert_eq!(after, new_commit); - assert!(!repo_path.join("to_delete.txt").exists()); -} - -#[test] -fn get_github_repo_info_parses_origin() { - let td = TempDir::new().unwrap(); - let repo_path = init_repo_main(&td); - let s = GitService::new(); - s.set_remote(&repo_path, "origin", "https://github.com/foo/bar.git") - .unwrap(); - let info = s.get_github_repo_info(&repo_path).unwrap(); - assert_eq!(info.owner, "foo"); - assert_eq!(info.repo_name, "bar"); -} - #[test] fn get_branch_diffs_between_branches() { let td = TempDir::new().unwrap(); @@ -386,8 +337,8 @@ fn get_branch_diffs_between_branches() { let _ = s.commit(&repo_path, "add a").unwrap(); // create branch and add new file - s.create_branch(&repo_path, "feature").unwrap(); - s.checkout_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); + checkout_branch(&repo_path, "feature"); write_file(&repo_path, "b.txt", "b\n"); let _ = s.commit(&repo_path, "add b").unwrap(); @@ -418,7 +369,7 @@ fn worktree_diff_respects_path_filter() { let _ = s.commit(&repo_path, "baseline").unwrap(); // create feature and work in place (worktree is repo_path) - s.create_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); // modify files without committing write_file(&repo_path, "src/only.txt", "only\n"); @@ -466,7 +417,7 @@ fn create_unicode_branch_and_list() { let _ = s.commit(&repo_path, "base"); // unicode/slash branch name (valid ref) let bname = "feature/ünicode"; - s.create_branch(&repo_path, bname).unwrap(); + create_branch(&repo_path, bname); let names: Vec<_> = s .get_all_branches(&repo_path) .unwrap() @@ -487,7 +438,7 @@ fn worktree_diff_permission_only_change() { write_file(&repo_path, "p.sh", "echo hi\n"); let _ = s.commit(&repo_path, "add p.sh").unwrap(); // create a feature branch baseline at HEAD - s.create_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); // change only the permission (chmod +x) let mut perms = std::fs::metadata(repo_path.join("p.sh")) @@ -515,67 +466,6 @@ fn worktree_diff_permission_only_change() { assert_eq!(d.old_content, d.new_content); } -#[test] -fn delete_with_uncommitted_changes_succeeds() { - let td = TempDir::new().unwrap(); - let repo_path = init_repo_main(&td); - let s = GitService::new(); - // baseline file and commit - write_file(&repo_path, "d.txt", "v1\n"); - let _ = s.commit(&repo_path, "add d").unwrap(); - let before = s.get_head_info(&repo_path).unwrap().oid; - // uncommitted change - write_file(&repo_path, "d.txt", "v2\n"); - // delete and commit - let new_sha = s.delete_file_and_commit(&repo_path, "d.txt").unwrap(); - assert_eq!(s.get_head_info(&repo_path).unwrap().oid, new_sha); - assert!(!repo_path.join("d.txt").exists()); - assert_ne!(before, new_sha); -} - -#[cfg(unix)] -#[test] -fn delete_symlink_and_commit() { - use std::os::unix::fs::symlink; - let td = TempDir::new().unwrap(); - let repo_path = init_repo_main(&td); - let s = GitService::new(); - // Create target and symlink, commit - write_file(&repo_path, "target.txt", "t\n"); - let _ = s.commit(&repo_path, "add target").unwrap(); - symlink(repo_path.join("target.txt"), repo_path.join("link.txt")).unwrap(); - let _ = s.commit(&repo_path, "add symlink").unwrap(); - let before = s.get_head_info(&repo_path).unwrap().oid; - // Delete symlink - let new_sha = s.delete_file_and_commit(&repo_path, "link.txt").unwrap(); - assert_eq!(s.get_head_info(&repo_path).unwrap().oid, new_sha); - assert!(!repo_path.join("link.txt").exists()); - assert_ne!(before, new_sha); -} - -#[test] -fn delete_file_commit_has_author_without_user() { - // Verify libgit2 path uses fallback author when no config exists - let td = TempDir::new().unwrap(); - let repo_path = td.path().join("repo_fallback_delete"); - let s = GitService::new(); - // No configure_user call; initial commit uses fallback signature too - s.initialize_repo_with_main_branch(&repo_path).unwrap(); - - // Create then delete an untracked file via service - write_file(&repo_path, "q.txt", "temp\n"); - let sha = s.delete_file_and_commit(&repo_path, "q.txt").unwrap(); - - // Author should be present: either global identity or fallback - let (name, email) = get_commit_author(&repo_path, &sha); - if has_global_git_identity() { - assert!(name.is_some() && email.is_some()); - } else { - assert_eq!(name.as_deref(), Some("Vibe Kanban")); - assert_eq!(email.as_deref(), Some("noreply@vibekanban.com")); - } -} - #[test] fn github_repo_info_parses_https_and_ssh_urls() { let info = GitHubRepoInfo::from_remote_url("https://github.com/owner/repo.git").unwrap(); @@ -611,7 +501,7 @@ fn squash_merge_libgit2_sets_author_without_user() { s.initialize_repo_with_main_branch(&repo_path).unwrap(); // Create feature branch and worktree - s.create_branch(&repo_path, "feature").unwrap(); + create_branch(&repo_path, "feature"); s.add_worktree(&repo_path, &worktree_path, "feature", false) .unwrap(); @@ -635,8 +525,8 @@ fn squash_merge_libgit2_sets_author_without_user() { } // Ensure main repo is NOT on base branch so merge_changes takes libgit2 path - s.create_branch(&repo_path, "dev").unwrap(); - s.checkout_branch(&repo_path, "dev").unwrap(); + create_branch(&repo_path, "dev"); + checkout_branch(&repo_path, "dev"); // Merge feature -> main (libgit2 squash) let merge_sha = s diff --git a/frontend/src/components/DiffCard.tsx b/frontend/src/components/DiffCard.tsx index 25f3fe66..f69a5efe 100644 --- a/frontend/src/components/DiffCard.tsx +++ b/frontend/src/components/DiffCard.tsx @@ -246,11 +246,10 @@ export default function DiffCard({ if (!selectedAttempt?.id) return; try { const openPath = newName || oldName; - const response = await attemptsApi.openEditor( - selectedAttempt.id, - undefined, - openPath || undefined - ); + const response = await attemptsApi.openEditor(selectedAttempt.id, { + editor_type: null, + file_path: openPath ?? null, + }); // If a URL is returned, open it in a new window/tab if (response.url) { diff --git a/frontend/src/hooks/useOpenInEditor.ts b/frontend/src/hooks/useOpenInEditor.ts index 957cf999..73013e9d 100644 --- a/frontend/src/hooks/useOpenInEditor.ts +++ b/frontend/src/hooks/useOpenInEditor.ts @@ -19,11 +19,10 @@ export function useOpenInEditor( const { editorType, filePath } = options ?? {}; try { - const response = await attemptsApi.openEditor( - attemptId, - editorType, - filePath - ); + const response = await attemptsApi.openEditor(attemptId, { + editor_type: editorType ?? null, + file_path: filePath ?? null, + }); // If a URL is returned, open it in a new window/tab if (response.url) { diff --git a/frontend/src/hooks/useOpenProjectInEditor.ts b/frontend/src/hooks/useOpenProjectInEditor.ts index 7e7ff79d..6f9e758b 100644 --- a/frontend/src/hooks/useOpenProjectInEditor.ts +++ b/frontend/src/hooks/useOpenProjectInEditor.ts @@ -12,7 +12,10 @@ export function useOpenProjectInEditor( if (!project) return; try { - const response = await projectsApi.openEditor(project.id, editorType); + const response = await projectsApi.openEditor(project.id, { + editor_type: editorType ?? null, + file_path: null, + }); // If a URL is returned, open it in a new window/tab if (response.url) { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4c2f128c..fa16fc2a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -14,12 +14,10 @@ import { CreateTag, DirectoryListResponse, DirectoryEntry, - EditorType, ExecutionProcess, GitBranch, Project, CreateProject, - RepositoryInfo, SearchResult, ShareTaskResponse, Task, @@ -68,10 +66,12 @@ import { Invitation, RemoteProject, ListInvitationsResponse, + CommitCompareResult, + OpenEditorResponse, + OpenEditorRequest, } from 'shared/types'; // Re-export types for convenience -export type { RepositoryInfo } from 'shared/types'; export type { UpdateFollowUpDraftRequest, UpdateRetryFollowUpDraftRequest, @@ -106,16 +106,6 @@ const makeRequest = async (url: string, options: RequestInit = {}) => { }); }; -export interface FollowUpResponse { - message: string; - actual_attempt_id: string; - created_new_attempt: boolean; -} - -export interface OpenEditorResponse { - url: string | null; -} - export type Ok = { success: true; data: T }; export type Err = { success: false; error: E | undefined; message?: string }; @@ -272,16 +262,11 @@ export const projectsApi = { openEditor: async ( id: string, - editorType?: EditorType + data: OpenEditorRequest ): Promise => { - const requestBody: { editor_type?: EditorType } = {}; - if (editorType) requestBody.editor_type = editorType; - const response = await makeRequest(`/api/projects/${id}/open-editor`, { method: 'POST', - body: JSON.stringify( - Object.keys(requestBody).length > 0 ? requestBody : null - ), + body: JSON.stringify(data), }); return handleApiResponse(response); }, @@ -340,11 +325,6 @@ export const projectsApi = { // Task Management APIs export const tasksApi = { - getAll: async (projectId: string): Promise => { - const response = await makeRequest(`/api/tasks?project_id=${projectId}`); - return handleApiResponse(response); - }, - getById: async (taskId: string): Promise => { const response = await makeRequest(`/api/tasks/${taskId}`); return handleApiResponse(response); @@ -452,26 +432,6 @@ export const attemptsApi = { return handleApiResponse(response); }, - replaceProcess: async ( - attemptId: string, - data: { - process_id: string; - prompt: string; - variant?: string | null; - force_when_dirty?: boolean; - perform_git_reset?: boolean; - } - ): Promise => { - const response = await makeRequest( - `/api/task-attempts/${attemptId}/replace-process`, - { - method: 'POST', - body: JSON.stringify(data), - } - ); - return handleApiResponse(response); - }, - followUp: async ( attemptId: string, data: CreateFollowUpAttempt @@ -557,37 +517,15 @@ export const attemptsApi = { return handleApiResponse(response); }, - deleteFile: async ( - attemptId: string, - fileToDelete: string - ): Promise => { - const response = await makeRequest( - `/api/task-attempts/${attemptId}/delete-file?file_path=${encodeURIComponent( - fileToDelete - )}`, - { - method: 'POST', - } - ); - return handleApiResponse(response); - }, - openEditor: async ( attemptId: string, - editorType?: EditorType, - filePath?: string + data: OpenEditorRequest ): Promise => { - const requestBody: { editor_type?: EditorType; file_path?: string } = {}; - if (editorType) requestBody.editor_type = editorType; - if (filePath) requestBody.file_path = filePath; - const response = await makeRequest( `/api/task-attempts/${attemptId}/open-editor`, { method: 'POST', - body: JSON.stringify( - Object.keys(requestBody).length > 0 ? requestBody : null - ), + body: JSON.stringify(data), } ); return handleApiResponse(response); @@ -717,13 +655,7 @@ export const commitsApi = { compareToHead: async ( attemptId: string, sha: string - ): Promise<{ - head_oid: string; - target_oid: string; - ahead_from_head: number; - behind_from_head: number; - is_linear: boolean; - }> => { + ): Promise => { const response = await makeRequest( `/api/task-attempts/${attemptId}/commit-compare?sha=${encodeURIComponent( sha @@ -735,15 +667,6 @@ export const commitsApi = { // Execution Process APIs export const executionProcessesApi = { - getExecutionProcesses: async ( - attemptId: string - ): Promise => { - const response = await makeRequest( - `/api/execution-processes?task_attempt_id=${attemptId}` - ); - return handleApiResponse(response); - }, - getDetails: async (processId: string): Promise => { const response = await makeRequest(`/api/execution-processes/${processId}`); return handleApiResponse(response); @@ -794,14 +717,6 @@ export const configApi = { }, }; -// GitHub APIs (only available in cloud mode) -export const githubApi = { - listRepositories: async (page: number = 1): Promise => { - const response = await makeRequest(`/api/github/repositories?page=${page}`); - return handleApiResponse(response); - }, -}; - // Task Tags APIs (all tags are global) export const tagsApi = { list: async (params?: TagSearchParams): Promise => { @@ -812,11 +727,6 @@ export const tagsApi = { return handleApiResponse(response); }, - get: async (tagId: string): Promise => { - const response = await makeRequest(`/api/tags/${tagId}`); - return handleApiResponse(response); - }, - create: async (data: CreateTag): Promise => { const response = await makeRequest('/api/tags', { method: 'POST', diff --git a/shared/types.ts b/shared/types.ts index b2553bd8..f84c4abd 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -152,6 +152,12 @@ export type RenameBranchRequest = { new_branch_name: string, }; export type RenameBranchResponse = { branch: string, }; +export type CommitCompareResult = { head_oid: string, target_oid: string, ahead_from_head: number, behind_from_head: number, is_linear: boolean, }; + +export type OpenEditorRequest = { editor_type: string | null, file_path: string | null, }; + +export type OpenEditorResponse = { url: string | null, }; + export type AssignSharedTaskRequest = { new_assignee_user_id: string | null, version: bigint | null, }; export type AssignSharedTaskResponse = { shared_task: SharedTask, }; @@ -198,8 +204,6 @@ additions: number | null, deletions: number | null, }; export type DiffChangeKind = "added" | "deleted" | "modified" | "renamed" | "copied" | "permissionChange"; -export type RepositoryInfo = { id: bigint, name: string, full_name: string, owner: string, description: string | null, clone_url: string, ssh_url: string, default_branch: string, private: boolean, }; - export type CommandBuilder = { /** * Base executable command (e.g., "npx -y @anthropic-ai/claude-code@latest") @@ -284,28 +288,6 @@ export type RebaseTaskAttemptRequest = { old_base_branch: string | null, new_bas export type GitOperationError = { "type": "merge_conflicts", message: string, op: ConflictOp, } | { "type": "rebase_in_progress" }; -export type ReplaceProcessRequest = { -/** - * Process to replace (delete this and later ones) - */ -process_id: string, -/** - * New prompt to use for the replacement follow-up - */ -prompt: string, -/** - * Optional variant override - */ -variant: string | null, -/** - * If true, allow resetting Git even when uncommitted changes exist - */ -force_when_dirty: boolean | null, -/** - * If false, skip performing the Git reset step (history drop still applies) - */ -perform_git_reset: boolean | null, }; - export type CommitInfo = { sha: string, subject: string, }; export type BranchStatus = { commits_behind: number | null, commits_ahead: number | null, has_uncommitted_changes: boolean | null, head_oid: string | null, uncommitted_count: number | null, untracked_count: number | null, target_branch_name: string, remote_commits_behind: number | null, remote_commits_ahead: number | null, merges: Array,