Refactor branch fetching to use repo IDs instead of project IDs (Vibe Kanban) (#1560)

* All tasks are complete. Here's a summary of the changes made:

## Summary

Successfully refactored the project branches API to fetch branches using repo IDs instead of project IDs.

### Files Created
- `frontend/src/hooks/useRepoBranches.ts` - New hook that fetches branches for a single repository using `repoApi.getBranches(repoId)`

### Files Modified

1. **`frontend/src/hooks/index.ts`** - Added export for `useRepoBranches`

2. **`frontend/src/components/dialogs/tasks/GitActionsDialog.tsx`**
   - Removed `useState` for `repoBranches` and `loadingBranches`
   - Removed `useEffect` that called `projectsApi.getBranches(effectiveProjectId)`
   - Removed `useMemo` that filtered branches by `selectedRepoId`
   - Now uses `useRepoBranches(selectedRepoId)` to fetch branches directly for the selected repo from the task attempt

3. **`frontend/src/components/dialogs/tasks/CreatePRDialog.tsx`**
   - Removed `projectId` from props interface and component destructuring
   - Removed `useState` for `branches` and `branchesLoading`
   - Removed branch-fetching logic from useEffect
   - Now uses `useRepoBranches(repoId)` to fetch branches directly for the specified repo
   - Split useEffect into two: one for form initialization, one for setting default base branch

4. **`frontend/src/components/tasks/Toolbar/GitOperations.tsx`**
   - Removed `projectId` from `CreatePRDialog.show()` call

### Benefits
- More efficient - fetches only branches for the specific repo needed instead of all project repos
- Better caching - React Query caches branches per repo for efficient reuse
- Cleaner code - removed manual filtering logic from components
- Type safety - hooks return `GitBranch[]` directly instead of `RepositoryBranches[]`

* All tasks complete. Here's a summary of the changes made:

## Summary

Successfully decoupled the branches API from project IDs.

### Changes Made

**1. `frontend/src/hooks/useBranches.ts`** - Refactored to accept `repos: Array<{ id: string; name: string }>` instead of `projectId`
- Now uses `useQueries` to fetch branches for multiple repos in parallel
- Reuses the same query keys as `useRepoBranches` for cache consistency

**2. `frontend/src/hooks/useRepoBranchSelection.ts`** - Refactored to accept `repos: Repo[]` instead of `projectId`
- Removed internal fetch of project repos (caller now provides them)
- Removed `projectRepos` from return value
- Uses refactored `useBranches(repos)`

**3. `frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx`**
- Added `useQuery` to fetch project repos separately
- Passes `repos` to `useRepoBranchSelection`

**4. `frontend/src/components/dialogs/tasks/TaskFormDialog.tsx`**
- Already fetched `projectRepos`
- Updated to pass `projectRepos` to refactored `useBranches`

**5. `frontend/src/components/layout/Navbar.tsx`**
- Replaced `useBranches(projectId)` with `useQuery` for `projectsApi.getRepositories`
- Only needed repo count, not branches

**6. `frontend/src/components/projects/ProjectCard.tsx`**
- Replaced `useBranches(project.id)` with `useQuery` for `projectsApi.getRepositories`
- Only needed repo count, not branches

### Benefits
- Branch hooks no longer require a project ID
- Repos can come from any source (project, attempt, or future sources without projects)
- Better separation of concerns
- Enables future task attempts without projects attached

* Cleanup script changes for task attempt 6b5b9208-565d-4620-9fe3-20791db77d7a

* The fix is already applied. Type checks pass with `Repo[]` instead of the loose `Array<{ id: string; name: string }>` type.

This makes it explicit that `useBranches` expects actual `Repo` objects (with `Repo.id` being the repository's UUID), not project-repo junction records.

* All done. The refactor is complete:

**Summary of changes:**

1. **Created `useProjectRepos` hook** (`frontend/src/hooks/useProjectRepos.ts`) - A reusable hook that fetches project repositories with proper query key management

2. **Updated 5 files** to use the new hook instead of duplicating the `useQuery` pattern:
   - `CreateAttemptDialog.tsx`
   - `TaskFormDialog.tsx`
   - `Navbar.tsx`
   - `ProjectCard.tsx`
   - `NoServerContent.tsx`

3. **Earlier refactors** (from before the summary):
   - `useBranches.ts` - Now accepts `repos: Repo[]` instead of `projectId`
   - `useRepoBranchSelection.ts` - Now accepts `repos: Repo[]` instead of `projectId`

This enables future task attempts without projects attached, as branch fetching is now decoupled from project IDs.

* No more usages. The branches in `ProjectTasks.tsx` are used for the diffs panel - showing available branches for git operations when viewing an attempt's diffs.

* Cleanup script changes for task attempt 6b5b9208-565d-4620-9fe3-20791db77d7a

* All done. The refactor is complete:

**Summary of changes:**

`GitOperations` now fetches its own branches using `useRepoBranches(selectedRepoId)` instead of receiving them as a prop. This:

1. Removes prop drilling through `ProjectTasks` → `DiffsPanelContainer` → `DiffsPanel` → `GitOperations`
2. Removes the need for `ProjectTasks` to fetch all project repos just to get branches for the selected repo
3. Makes `GitOperations` self-contained - it already has `selectedRepoId` from `useAttemptRepo`, so it can fetch its own branches
4. Simplifies `GitActionsDialog` which no longer needs to fetch branches either

* All done. Here's a summary of the changes:

**Removed `projectId` from GitOperations flow:**

1. **`useRebase.ts`** - Changed param from `projectId` to `repoId`, updated invalidation to use `repoBranchKeys.byRepo(repoId)`

2. **`useChangeTargetBranch.ts`** - Changed param from `projectId` to `repoId`, updated invalidation to use `repoBranchKeys.byRepo(repoId)`

3. **`useMerge.ts`** - Updated invalidation to use `repoBranchKeys.all` instead of `['projectBranches']`

4. **`useGitOperations.ts`** - Changed param from `projectId` to `repoId`

5. **`GitOperations.tsx`** - Removed `projectId` prop, now passes `selectedRepoId` to `useGitOperations`

6. **`GitActionsDialog.tsx`** - Removed `projectId` from props and internal logic

7. **`DiffsPanelContainer`** (in ProjectTasks.tsx) - Removed `projectId` prop

8. **Callers of `GitActionsDialog.show`** - Removed `projectId` from `NextActionCard.tsx` and `actions-dropdown.tsx`

* The cleanup is complete. Here's a summary of what was done:

**Removed dead project branches code:**

1. **Backend** (`crates/server/src/routes/projects.rs`):
   - Removed `get_project_branches` handler and route
   - Removed `RepositoryBranches` and `ProjectBranchesResponse` structs
   - Fixed unused `Serialize` import warning

2. **Type Generation** (`crates/server/src/bin/generate_types.rs`):
   - Removed exports for `ProjectBranchesResponse` and `RepositoryBranches`

3. **Frontend Hooks** (`frontend/src/hooks/useBranches.ts`):
   - Defined local `RepositoryBranches` type instead of importing from shared/types

4. **Frontend API** (`frontend/src/lib/api.ts`):
   - Removed `projectsApi.getBranches` method
   - Removed `RepositoryBranches` and `ProjectBranchesResponse` imports

Both TypeScript and Rust compilation pass without errors.

* Done. Removed the `RepositoryBranches` type and simplified `useBranches` to return `Map<string, GitBranch[]>`. Changes:

1. **`useBranches.ts`** - Returns `Map<string, GitBranch[]>` instead of `RepositoryBranches[]`
2. **`useRepoBranchSelection.ts`** - Uses `branchMap.get(repo.id) ?? []` instead of `.find()`
3. **`TaskFormDialog.tsx`** - Uses `branchMap.get(repo.id) ?? []` instead of `.find()`

Type checks pass.

* Done. Deleted `useBranches` entirely and consolidated everything into `useRepoBranchSelection`. Summary:

1. **Deleted** `frontend/src/hooks/useBranches.ts`
2. **Updated** `useRepoBranchSelection.ts` - inlined the `useQueries` logic directly
3. **Updated** `hooks/index.ts` - removed `useBranches` export
4. **Updated** `TaskFormDialog.tsx` - now uses `useRepoBranchSelection` instead of duplicating the config-building logic

Type checks pass.

* types
This commit is contained in:
Alex Netsch
2025-12-16 14:21:42 +00:00
committed by GitHub
parent ec8666da74
commit f989b47470
25 changed files with 137 additions and 309 deletions

View File

@@ -103,8 +103,6 @@ fn generate_types_content() -> String {
server::routes::config::CheckEditorAvailabilityResponse::decl(), server::routes::config::CheckEditorAvailabilityResponse::decl(),
server::routes::config::CheckAgentAvailabilityQuery::decl(), server::routes::config::CheckAgentAvailabilityQuery::decl(),
server::routes::oauth::CurrentUserResponse::decl(), server::routes::oauth::CurrentUserResponse::decl(),
server::routes::projects::RepositoryBranches::decl(),
server::routes::projects::ProjectBranchesResponse::decl(),
server::routes::task_attempts::CreateFollowUpAttempt::decl(), server::routes::task_attempts::CreateFollowUpAttempt::decl(),
server::routes::task_attempts::ChangeTargetBranchRequest::decl(), server::routes::task_attempts::ChangeTargetBranchRequest::decl(),
server::routes::task_attempts::ChangeTargetBranchResponse::decl(), server::routes::task_attempts::ChangeTargetBranchResponse::decl(),

View File

@@ -14,9 +14,9 @@ use db::models::{
repo::Repo, repo::Repo,
}; };
use deployment::Deployment; use deployment::Deployment;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use services::services::{ use services::services::{
file_search_cache::SearchQuery, git::GitBranch, project::ProjectServiceError, file_search_cache::SearchQuery, project::ProjectServiceError,
remote_client::CreateRemoteProjectPayload, remote_client::CreateRemoteProjectPayload,
}; };
use ts_rs::TS; use ts_rs::TS;
@@ -28,20 +28,6 @@ use uuid::Uuid;
use crate::{DeploymentImpl, error::ApiError, middleware::load_project_middleware}; use crate::{DeploymentImpl, error::ApiError, middleware::load_project_middleware};
/// Branches for a single repository
#[derive(Debug, Serialize, TS)]
pub struct RepositoryBranches {
pub repository_id: Uuid,
pub repository_name: String,
pub branches: Vec<GitBranch>,
}
/// Response containing branches grouped by repository
#[derive(Debug, Serialize, TS)]
pub struct ProjectBranchesResponse {
pub repositories: Vec<RepositoryBranches>,
}
#[derive(Deserialize, TS)] #[derive(Deserialize, TS)]
pub struct LinkToExistingRequest { pub struct LinkToExistingRequest {
pub remote_project_id: Uuid, pub remote_project_id: Uuid,
@@ -66,33 +52,6 @@ pub async fn get_project(
Ok(ResponseJson(ApiResponse::success(project))) Ok(ResponseJson(ApiResponse::success(project)))
} }
pub async fn get_project_branches(
Extension(project): Extension<Project>,
State(deployment): State<DeploymentImpl>,
) -> Result<ResponseJson<ApiResponse<ProjectBranchesResponse>>, ApiError> {
let repositories = deployment
.project()
.get_repositories(&deployment.db().pool, project.id)
.await?;
let mut repo_branches = Vec::with_capacity(repositories.len());
for repo in repositories {
let branches = deployment.git().get_all_branches(&repo.path)?;
repo_branches.push(RepositoryBranches {
repository_id: repo.id,
repository_name: repo.name,
branches,
});
}
Ok(ResponseJson(ApiResponse::success(
ProjectBranchesResponse {
repositories: repo_branches,
},
)))
}
pub async fn link_project_to_existing_remote( pub async fn link_project_to_existing_remote(
Extension(project): Extension<Project>, Extension(project): Extension<Project>,
State(deployment): State<DeploymentImpl>, State(deployment): State<DeploymentImpl>,
@@ -583,7 +542,6 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
get(get_project).put(update_project).delete(delete_project), get(get_project).put(update_project).delete(delete_project),
) )
.route("/remote/members", get(get_project_remote_members)) .route("/remote/members", get(get_project_remote_members))
.route("/branches", get(get_project_branches))
.route("/search", get(search_project_files)) .route("/search", get(search_project_files))
.route("/open-editor", post(open_project_in_editor)) .route("/open-editor", post(open_project_in_editor))
.route( .route(

View File

@@ -122,9 +122,8 @@ export function NextActionCard({
GitActionsDialog.show({ GitActionsDialog.show({
attemptId, attemptId,
task, task,
projectId: project?.id,
}); });
}, [attemptId, task, project?.id]); }, [attemptId, task]);
const handleRunSetup = useCallback(async () => { const handleRunSetup = useCallback(async () => {
if (!attemptId || !attempt) return; if (!attemptId || !attempt) return;

View File

@@ -18,6 +18,7 @@ import {
useAttempt, useAttempt,
useRepoBranchSelection, useRepoBranchSelection,
useTaskAttempts, useTaskAttempts,
useProjectRepos,
} from '@/hooks'; } from '@/hooks';
import { useProject } from '@/contexts/ProjectContext'; import { useProject } from '@/contexts/ProjectContext';
import { useUserSystem } from '@/components/ConfigProvider'; import { useUserSystem } from '@/components/ConfigProvider';
@@ -66,17 +67,19 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
{ enabled: modal.visible && !!parentAttemptId } { enabled: modal.visible && !!parentAttemptId }
); );
const { data: projectRepos = [], isLoading: isLoadingRepos } =
useProjectRepos(projectId, { enabled: modal.visible });
const { const {
configs: repoBranchConfigs, configs: repoBranchConfigs,
projectRepos,
isLoading: isLoadingBranches, isLoading: isLoadingBranches,
setRepoBranch, setRepoBranch,
getAttemptRepoInputs, getAttemptRepoInputs,
reset: resetBranchSelection, reset: resetBranchSelection,
} = useRepoBranchSelection({ } = useRepoBranchSelection({
projectId, repos: projectRepos,
initialBranch: parentAttempt?.branch, initialBranch: parentAttempt?.branch,
enabled: modal.visible, enabled: modal.visible && projectRepos.length > 0,
}); });
const latestAttempt = useMemo(() => { const latestAttempt = useMemo(() => {
@@ -118,6 +121,7 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
const effectiveProfile = userSelectedProfile ?? defaultProfile; const effectiveProfile = userSelectedProfile ?? defaultProfile;
const isLoadingInitial = const isLoadingInitial =
isLoadingRepos ||
isLoadingBranches || isLoadingBranches ||
isLoadingAttempts || isLoadingAttempts ||
isLoadingTask || isLoadingTask ||

View File

@@ -17,16 +17,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { attemptsApi } from '@/lib/api.ts'; import { attemptsApi } from '@/lib/api.ts';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
GitBranch,
RepositoryBranches,
TaskAttempt,
TaskWithAttemptStatus,
} from 'shared/types';
import { projectsApi } from '@/lib/api.ts';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import NiceModal, { useModal } from '@ebay/nice-modal-react'; import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { useAuth } from '@/hooks'; import { useAuth, useRepoBranches } from '@/hooks';
import { import {
GhCliHelpInstructions, GhCliHelpInstructions,
GhCliSetupDialog, GhCliSetupDialog,
@@ -43,12 +37,11 @@ import { defineModal } from '@/lib/modals';
interface CreatePRDialogProps { interface CreatePRDialogProps {
attempt: TaskAttempt; attempt: TaskAttempt;
task: TaskWithAttemptStatus; task: TaskWithAttemptStatus;
projectId: string;
repoId: string; repoId: string;
} }
const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>( const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
({ attempt, task, projectId, repoId }) => { ({ attempt, task, repoId }) => {
const modal = useModal(); const modal = useModal();
const { t } = useTranslation('tasks'); const { t } = useTranslation('tasks');
const { isLoaded } = useAuth(); const { isLoaded } = useAuth();
@@ -61,18 +54,22 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
const [ghCliHelp, setGhCliHelp] = useState<GhCliSupportContent | null>( const [ghCliHelp, setGhCliHelp] = useState<GhCliSupportContent | null>(
null null
); );
const [branches, setBranches] = useState<GitBranch[]>([]);
const [branchesLoading, setBranchesLoading] = useState(false);
const [isDraft, setIsDraft] = useState(false); const [isDraft, setIsDraft] = useState(false);
const [autoGenerateDescription, setAutoGenerateDescription] = useState( const [autoGenerateDescription, setAutoGenerateDescription] = useState(
config?.pr_auto_description_enabled ?? false config?.pr_auto_description_enabled ?? false
); );
const { data: branches = [], isLoading: branchesLoading } = useRepoBranches(
repoId,
{ enabled: modal.visible && !!repoId }
);
const getGhCliHelpTitle = (variant: GhCliSupportVariant) => const getGhCliHelpTitle = (variant: GhCliSupportVariant) =>
variant === 'homebrew' variant === 'homebrew'
? 'Homebrew is required for automatic setup' ? 'Homebrew is required for automatic setup'
: 'GitHub CLI needs manual setup'; : 'GitHub CLI needs manual setup';
// Initialize form when dialog opens
useEffect(() => { useEffect(() => {
if (!modal.visible || !isLoaded) { if (!modal.visible || !isLoaded) {
return; return;
@@ -80,32 +77,19 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
setPrTitle(`${task.title} (vibe-kanban)`); setPrTitle(`${task.title} (vibe-kanban)`);
setPrBody(task.description || ''); setPrBody(task.description || '');
setError(null);
// Always fetch branches for dropdown population
if (projectId) {
setBranchesLoading(true);
projectsApi
.getBranches(projectId)
.then((repoBranches: RepositoryBranches[]) => {
const repoData = repoBranches.find(
(r) => r.repository_id === repoId
);
const branchesForRepo = repoData?.branches ?? [];
setBranches(branchesForRepo);
// Set smart default: current branch (target_branch is now per-repo in AttemptRepo)
const currentBranch = branchesForRepo.find((b) => b.is_current);
if (currentBranch) {
setPrBaseBranch(currentBranch.name);
}
})
.catch(console.error)
.finally(() => setBranchesLoading(false));
}
setError(null); // Reset error when opening
setGhCliHelp(null); setGhCliHelp(null);
}, [modal.visible, isLoaded, task, attempt, projectId, repoId]); }, [modal.visible, isLoaded, task]);
// Set default base branch when branches are loaded
useEffect(() => {
if (branches.length > 0 && !prBaseBranch) {
const currentBranch = branches.find((b) => b.is_current);
if (currentBranch) {
setPrBaseBranch(currentBranch.name);
}
}
}, [branches, prBaseBranch]);
const isMacEnvironment = useMemo( const isMacEnvironment = useMemo(
() => environment?.os_type?.toLowerCase().includes('mac'), () => environment?.os_type?.toLowerCase().includes('mac'),
@@ -113,7 +97,7 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
); );
const handleConfirmCreatePR = useCallback(async () => { const handleConfirmCreatePR = useCallback(async () => {
if (!projectId || !attempt.id) return; if (!repoId || !attempt.id) return;
setError(null); setError(null);
setGhCliHelp(null); setGhCliHelp(null);
@@ -227,7 +211,7 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
} }
}, [ }, [
attempt, attempt,
projectId, repoId,
prBaseBranch, prBaseBranch,
prBody, prBody,
prTitle, prTitle,
@@ -237,7 +221,6 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
modal, modal,
isMacEnvironment, isMacEnvironment,
t, t,
repoId,
]); ]);
const handleCancelCreatePR = useCallback(() => { const handleCancelCreatePR = useCallback(() => {

View File

@@ -1,4 +1,3 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ExternalLink, GitPullRequest } from 'lucide-react'; import { ExternalLink, GitPullRequest } from 'lucide-react';
import { import {
@@ -12,41 +11,28 @@ import GitOperations from '@/components/tasks/Toolbar/GitOperations';
import { useTaskAttempt } from '@/hooks/useTaskAttempt'; import { useTaskAttempt } from '@/hooks/useTaskAttempt';
import { useBranchStatus, useAttemptExecution } from '@/hooks'; import { useBranchStatus, useAttemptExecution } from '@/hooks';
import { useAttemptRepo } from '@/hooks/useAttemptRepo'; import { useAttemptRepo } from '@/hooks/useAttemptRepo';
import { useProject } from '@/contexts/ProjectContext';
import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext';
import { import {
GitOperationsProvider, GitOperationsProvider,
useGitOperationsError, useGitOperationsError,
} from '@/contexts/GitOperationsContext'; } from '@/contexts/GitOperationsContext';
import { projectsApi } from '@/lib/api'; import type { Merge, TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import type {
GitBranch,
Merge,
RepositoryBranches,
TaskAttempt,
TaskWithAttemptStatus,
} from 'shared/types';
import NiceModal, { useModal } from '@ebay/nice-modal-react'; import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { defineModal } from '@/lib/modals'; import { defineModal } from '@/lib/modals';
export interface GitActionsDialogProps { export interface GitActionsDialogProps {
attemptId: string; attemptId: string;
task?: TaskWithAttemptStatus; task?: TaskWithAttemptStatus;
projectId?: string;
} }
interface GitActionsDialogContentProps { interface GitActionsDialogContentProps {
attempt: TaskAttempt; attempt: TaskAttempt;
task: TaskWithAttemptStatus; task: TaskWithAttemptStatus;
projectId: string;
branches: GitBranch[];
} }
function GitActionsDialogContent({ function GitActionsDialogContent({
attempt, attempt,
task, task,
projectId,
branches,
}: GitActionsDialogContentProps) { }: GitActionsDialogContentProps) {
const { t } = useTranslation('tasks'); const { t } = useTranslation('tasks');
const { data: branchStatus } = useBranchStatus(attempt.id); const { data: branchStatus } = useBranchStatus(attempt.id);
@@ -96,9 +82,7 @@ function GitActionsDialogContent({
<GitOperations <GitOperations
selectedAttempt={attempt} selectedAttempt={attempt}
task={task} task={task}
projectId={projectId}
branchStatus={branchStatus ?? null} branchStatus={branchStatus ?? null}
branches={branches}
isAttemptRunning={isAttemptRunning} isAttemptRunning={isAttemptRunning}
selectedBranch={getSelectedRepoStatus()?.target_branch_name ?? null} selectedBranch={getSelectedRepoStatus()?.target_branch_name ?? null}
layout="vertical" layout="vertical"
@@ -108,34 +92,11 @@ function GitActionsDialogContent({
} }
const GitActionsDialogImpl = NiceModal.create<GitActionsDialogProps>( const GitActionsDialogImpl = NiceModal.create<GitActionsDialogProps>(
({ attemptId, task, projectId: providedProjectId }) => { ({ attemptId, task }) => {
const modal = useModal(); const modal = useModal();
const { t } = useTranslation('tasks'); const { t } = useTranslation('tasks');
const { project } = useProject();
const effectiveProjectId = providedProjectId ?? project?.id;
const { data: attempt } = useTaskAttempt(attemptId); const { data: attempt } = useTaskAttempt(attemptId);
const { selectedRepoId } = useAttemptRepo(attemptId);
const [repoBranches, setRepoBranches] = useState<RepositoryBranches[]>([]);
const [loadingBranches, setLoadingBranches] = useState(true);
useEffect(() => {
if (!effectiveProjectId) return;
setLoadingBranches(true);
projectsApi
.getBranches(effectiveProjectId)
.then(setRepoBranches)
.catch(() => setRepoBranches([]))
.finally(() => setLoadingBranches(false));
}, [effectiveProjectId]);
const branches = useMemo(
() =>
repoBranches.find((r) => r.repository_id === selectedRepoId)
?.branches ?? [],
[repoBranches, selectedRepoId]
);
const handleOpenChange = (open: boolean) => { const handleOpenChange = (open: boolean) => {
if (!open) { if (!open) {
@@ -143,8 +104,7 @@ const GitActionsDialogImpl = NiceModal.create<GitActionsDialogProps>(
} }
}; };
const isLoading = const isLoading = !attempt || !task;
!attempt || !effectiveProjectId || loadingBranches || !task;
return ( return (
<Dialog open={modal.visible} onOpenChange={handleOpenChange}> <Dialog open={modal.visible} onOpenChange={handleOpenChange}>
@@ -163,12 +123,7 @@ const GitActionsDialogImpl = NiceModal.create<GitActionsDialogProps>(
key={attempt.id} key={attempt.id}
attemptId={attempt.id} attemptId={attempt.id}
> >
<GitActionsDialogContent <GitActionsDialogContent attempt={attempt} task={task} />
attempt={attempt}
task={task}
projectId={effectiveProjectId}
branches={branches}
/>
</ExecutionProcessesProvider> </ExecutionProcessesProvider>
</GitOperationsProvider> </GitOperationsProvider>
)} )}

View File

@@ -31,11 +31,11 @@ import RepoBranchSelector from '@/components/tasks/RepoBranchSelector';
import { ExecutorProfileSelector } from '@/components/settings'; import { ExecutorProfileSelector } from '@/components/settings';
import { useUserSystem } from '@/components/ConfigProvider'; import { useUserSystem } from '@/components/ConfigProvider';
import { import {
useBranches,
useTaskImages, useTaskImages,
useImageUpload, useImageUpload,
useTaskMutations, useTaskMutations,
type RepoBranchConfig, useProjectRepos,
useRepoBranchSelection,
} from '@/hooks'; } from '@/hooks';
import { import {
useKeySubmitTask, useKeySubmitTask,
@@ -50,8 +50,6 @@ import type {
ExecutorProfileId, ExecutorProfileId,
ImageResponse, ImageResponse,
} from 'shared/types'; } from 'shared/types';
import { projectsApi } from '@/lib/api';
import { useQuery } from '@tanstack/react-query';
interface Task { interface Task {
id: string; id: string;
@@ -104,43 +102,20 @@ const TaskFormDialogImpl = NiceModal.create<TaskFormDialogProps>((props) => {
const [showDiscardWarning, setShowDiscardWarning] = useState(false); const [showDiscardWarning, setShowDiscardWarning] = useState(false);
const forceCreateOnlyRef = useRef(false); const forceCreateOnlyRef = useRef(false);
const { data: repoBranches, isLoading: branchesLoading } =
useBranches(projectId);
const { data: taskImages } = useTaskImages( const { data: taskImages } = useTaskImages(
editMode ? props.task.id : undefined editMode ? props.task.id : undefined
); );
const { data: projectRepos = [] } = useQuery({ const { data: projectRepos = [] } = useProjectRepos(projectId, {
queryKey: ['projectRepositories', projectId],
queryFn: () => projectsApi.getRepositories(projectId),
enabled: modal.visible, enabled: modal.visible,
}); });
const initialBranch =
const repoBranchConfigs = useMemo((): RepoBranchConfig[] => { mode === 'subtask' ? props.initialBaseBranch : undefined;
return projectRepos.map((repo) => { const { configs: repoBranchConfigs, isLoading: branchesLoading } =
const repoBranchData = repoBranches?.find( useRepoBranchSelection({
(rb) => rb.repository_id === repo.id repos: projectRepos,
); initialBranch,
const branches = repoBranchData?.branches ?? []; enabled: modal.visible && projectRepos.length > 0,
let targetBranch: string | null = null;
const initialBranch =
mode === 'subtask' ? props.initialBaseBranch : undefined;
if (initialBranch && branches.some((b) => b.name === initialBranch)) {
targetBranch = initialBranch;
} else {
const currentBranch = branches.find((b) => b.is_current);
targetBranch = currentBranch?.name ?? branches[0]?.name ?? null;
}
return {
repoId: repo.id,
repoDisplayName: repo.display_name,
targetBranch,
branches,
};
}); });
}, [projectRepos, repoBranches, mode, props]);
const defaultRepoBranches = useMemo((): RepoBranch[] => { const defaultRepoBranches = useMemo((): RepoBranch[] => {
return repoBranchConfigs return repoBranchConfigs

View File

@@ -27,7 +27,7 @@ import { openTaskForm } from '@/lib/openTaskForm';
import { useProject } from '@/contexts/ProjectContext'; import { useProject } from '@/contexts/ProjectContext';
import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor'; import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor';
import { OpenInIdeButton } from '@/components/ide/OpenInIdeButton'; import { OpenInIdeButton } from '@/components/ide/OpenInIdeButton';
import { useBranches } from '@/hooks/useBranches'; import { useProjectRepos } from '@/hooks';
import { useDiscordOnlineCount } from '@/hooks/useDiscordOnlineCount'; import { useDiscordOnlineCount } from '@/hooks/useDiscordOnlineCount';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
@@ -80,7 +80,7 @@ export function Navbar() {
const { data: onlineCount } = useDiscordOnlineCount(); const { data: onlineCount } = useDiscordOnlineCount();
const { loginStatus, reloadSystem } = useUserSystem(); const { loginStatus, reloadSystem } = useUserSystem();
const { data: repos } = useBranches(projectId); const { data: repos } = useProjectRepos(projectId);
const isSingleRepoProject = repos?.length === 1; const isSingleRepoProject = repos?.length === 1;
const setSearchBarRef = useCallback( const setSearchBarRef = useCallback(

View File

@@ -24,12 +24,11 @@ import {
import { Project } from 'shared/types'; import { Project } from 'shared/types';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor'; import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor';
import { useNavigateWithSearch } from '@/hooks'; import { useNavigateWithSearch, useProjectRepos } from '@/hooks';
import { projectsApi } from '@/lib/api'; import { projectsApi } from '@/lib/api';
import { LinkProjectDialog } from '@/components/dialogs/projects/LinkProjectDialog'; import { LinkProjectDialog } from '@/components/dialogs/projects/LinkProjectDialog';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useProjectMutations } from '@/hooks/useProjectMutations'; import { useProjectMutations } from '@/hooks/useProjectMutations';
import { useBranches } from '@/hooks/useBranches';
type Props = { type Props = {
project: Project; project: Project;
@@ -51,7 +50,7 @@ function ProjectCard({
const handleOpenInEditor = useOpenProjectInEditor(project); const handleOpenInEditor = useOpenProjectInEditor(project);
const { t } = useTranslation('projects'); const { t } = useTranslation('projects');
const { data: repos } = useBranches(project.id); const { data: repos } = useProjectRepos(project.id);
const isSingleRepoProject = repos?.length === 1; const isSingleRepoProject = repos?.length === 1;
const { unlinkProject } = useProjectMutations({ const { unlinkProject } = useProjectMutations({

View File

@@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { import {
Play, Play,
Edit3, Edit3,
@@ -21,7 +20,7 @@ import {
import { useUserSystem } from '@/components/ConfigProvider'; import { useUserSystem } from '@/components/ConfigProvider';
import { useTaskMutations } from '@/hooks/useTaskMutations'; import { useTaskMutations } from '@/hooks/useTaskMutations';
import { useProjectMutations } from '@/hooks/useProjectMutations'; import { useProjectMutations } from '@/hooks/useProjectMutations';
import { projectsApi } from '@/lib/api'; import { useProjectRepos } from '@/hooks';
import { import {
COMPANION_INSTALL_TASK_TITLE, COMPANION_INSTALL_TASK_TITLE,
COMPANION_INSTALL_TASK_DESCRIPTION, COMPANION_INSTALL_TASK_DESCRIPTION,
@@ -53,14 +52,7 @@ export function NoServerContent({
const { createAndStart } = useTaskMutations(project?.id); const { createAndStart } = useTaskMutations(project?.id);
const { updateProject } = useProjectMutations(); const { updateProject } = useProjectMutations();
const { data: projectRepos = [] } = useQuery({ const { data: projectRepos = [] } = useProjectRepos(project?.id);
queryKey: ['projectRepositories', project?.id],
queryFn: () =>
project?.id
? projectsApi.getRepositories(project.id)
: Promise.resolve([]),
enabled: !!project?.id,
});
// Create strategy-based placeholders // Create strategy-based placeholders
const placeholders = system.environment const placeholders = system.environment

View File

@@ -19,7 +19,6 @@ import { useCallback, useMemo, useState } from 'react';
import type { import type {
RepoBranchStatus, RepoBranchStatus,
Merge, Merge,
GitBranch,
TaskAttempt, TaskAttempt,
TaskWithAttemptStatus, TaskWithAttemptStatus,
} from 'shared/types'; } from 'shared/types';
@@ -30,13 +29,12 @@ import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAttemptRepo } from '@/hooks/useAttemptRepo'; import { useAttemptRepo } from '@/hooks/useAttemptRepo';
import { useGitOperations } from '@/hooks/useGitOperations'; import { useGitOperations } from '@/hooks/useGitOperations';
import { useRepoBranches } from '@/hooks';
interface GitOperationsProps { interface GitOperationsProps {
selectedAttempt: TaskAttempt; selectedAttempt: TaskAttempt;
task: TaskWithAttemptStatus; task: TaskWithAttemptStatus;
projectId: string;
branchStatus: RepoBranchStatus[] | null; branchStatus: RepoBranchStatus[] | null;
branches: GitBranch[];
isAttemptRunning: boolean; isAttemptRunning: boolean;
selectedBranch: string | null; selectedBranch: string | null;
layout?: 'horizontal' | 'vertical'; layout?: 'horizontal' | 'vertical';
@@ -47,19 +45,18 @@ export type GitOperationsInputs = Omit<GitOperationsProps, 'selectedAttempt'>;
function GitOperations({ function GitOperations({
selectedAttempt, selectedAttempt,
task, task,
projectId,
branchStatus, branchStatus,
branches,
isAttemptRunning, isAttemptRunning,
selectedBranch, selectedBranch,
layout = 'horizontal', layout = 'horizontal',
}: GitOperationsProps) { }: GitOperationsProps) {
const { t } = useTranslation('tasks'); const { t } = useTranslation('tasks');
const git = useGitOperations(selectedAttempt.id, projectId);
const { repos, selectedRepoId, setSelectedRepoId } = useAttemptRepo( const { repos, selectedRepoId, setSelectedRepoId } = useAttemptRepo(
selectedAttempt.id selectedAttempt.id
); );
const git = useGitOperations(selectedAttempt.id, selectedRepoId ?? undefined);
const { data: branches = [] } = useRepoBranches(selectedRepoId);
const isChangingTargetBranch = git.states.changeTargetBranchPending; const isChangingTargetBranch = git.states.changeTargetBranchPending;
// Local state for git operations // Local state for git operations
@@ -256,7 +253,6 @@ function GitOperations({
CreatePRDialog.show({ CreatePRDialog.show({
attempt: selectedAttempt, attempt: selectedAttempt,
task, task,
projectId,
repoId: getSelectedRepoId(), repoId: getSelectedRepoId(),
}); });
}; };

View File

@@ -128,7 +128,6 @@ export function ActionsDropdown({
GitActionsDialog.show({ GitActionsDialog.show({
attemptId: attempt.id, attemptId: attempt.id,
task, task,
projectId,
}); });
}; };

View File

@@ -16,7 +16,8 @@ export { useNavigateWithSearch } from './useNavigateWithSearch';
export { useGitOperations } from './useGitOperations'; export { useGitOperations } from './useGitOperations';
export { useTask } from './useTask'; export { useTask } from './useTask';
export { useAttempt } from './useAttempt'; export { useAttempt } from './useAttempt';
export { useBranches } from './useBranches'; export { useRepoBranches } from './useRepoBranches';
export { useProjectRepos } from './useProjectRepos';
export { useRepoBranchSelection } from './useRepoBranchSelection'; export { useRepoBranchSelection } from './useRepoBranchSelection';
export type { RepoBranchConfig } from './useRepoBranchSelection'; export type { RepoBranchConfig } from './useRepoBranchSelection';
export { useTaskAttempts } from './useTaskAttempts'; export { useTaskAttempts } from './useTaskAttempts';

View File

@@ -1,25 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { projectsApi } from '@/lib/api';
import type { RepositoryBranches } from 'shared/types';
export const branchKeys = {
all: ['projectBranches'] as const,
byProject: (projectId: string | undefined) =>
['projectBranches', projectId] as const,
};
type Options = {
enabled?: boolean;
};
export function useBranches(projectId?: string, opts?: Options) {
const enabled = (opts?.enabled ?? true) && !!projectId;
return useQuery<RepositoryBranches[]>({
queryKey: branchKeys.byProject(projectId),
queryFn: () => projectsApi.getBranches(projectId!),
enabled,
staleTime: 60_000,
refetchOnWindowFocus: true,
});
}

View File

@@ -4,6 +4,7 @@ import type {
ChangeTargetBranchRequest, ChangeTargetBranchRequest,
ChangeTargetBranchResponse, ChangeTargetBranchResponse,
} from 'shared/types'; } from 'shared/types';
import { repoBranchKeys } from './useRepoBranches';
type ChangeTargetBranchParams = { type ChangeTargetBranchParams = {
newTargetBranch: string; newTargetBranch: string;
@@ -12,7 +13,7 @@ type ChangeTargetBranchParams = {
export function useChangeTargetBranch( export function useChangeTargetBranch(
attemptId: string | undefined, attemptId: string | undefined,
projectId: string | undefined, repoId: string | undefined,
onSuccess?: (data: ChangeTargetBranchResponse) => void, onSuccess?: (data: ChangeTargetBranchResponse) => void,
onError?: (err: unknown) => void onError?: (err: unknown) => void
) { ) {
@@ -45,9 +46,9 @@ export function useChangeTargetBranch(
}); });
} }
if (projectId) { if (repoId) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['projectBranches', projectId], queryKey: repoBranchKeys.byRepo(repoId),
}); });
} }

View File

@@ -10,13 +10,13 @@ import { ForcePushDialog } from '@/components/dialogs/git/ForcePushDialog';
export function useGitOperations( export function useGitOperations(
attemptId: string | undefined, attemptId: string | undefined,
projectId: string | undefined repoId: string | undefined
) { ) {
const { setError } = useGitOperationsError(); const { setError } = useGitOperationsError();
const rebase = useRebase( const rebase = useRebase(
attemptId, attemptId,
projectId, repoId,
() => setError(null), () => setError(null),
(err: Result<void, GitOperationError>) => { (err: Result<void, GitOperationError>) => {
if (!err.success) { if (!err.success) {
@@ -78,7 +78,7 @@ export function useGitOperations(
const changeTargetBranch = useChangeTargetBranch( const changeTargetBranch = useChangeTargetBranch(
attemptId, attemptId,
projectId, repoId,
() => setError(null), () => setError(null),
(err: unknown) => { (err: unknown) => {
const message = const message =

View File

@@ -1,5 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api'; import { attemptsApi } from '@/lib/api';
import { repoBranchKeys } from './useRepoBranches';
type MergeParams = { type MergeParams = {
repoId: string; repoId: string;
@@ -23,8 +24,8 @@ export function useMerge(
// Refresh attempt-specific branch information // Refresh attempt-specific branch information
queryClient.invalidateQueries({ queryKey: ['branchStatus', attemptId] }); queryClient.invalidateQueries({ queryKey: ['branchStatus', attemptId] });
// Invalidate all project branches queries // Invalidate all repo branches queries
queryClient.invalidateQueries({ queryKey: ['projectBranches'] }); queryClient.invalidateQueries({ queryKey: repoBranchKeys.all });
onSuccess?.(); onSuccess?.();
}, },

View File

@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { projectsApi } from '@/lib/api';
import type { Repo } from 'shared/types';
type Options = {
enabled?: boolean;
};
export function useProjectRepos(projectId?: string, opts?: Options) {
const enabled = (opts?.enabled ?? true) && !!projectId;
return useQuery<Repo[]>({
queryKey: ['projectRepositories', projectId],
queryFn: () => projectsApi.getRepositories(projectId!),
enabled,
});
}

View File

@@ -2,10 +2,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi, Result } from '@/lib/api'; import { attemptsApi, Result } from '@/lib/api';
import type { RebaseTaskAttemptRequest } from 'shared/types'; import type { RebaseTaskAttemptRequest } from 'shared/types';
import type { GitOperationError } from 'shared/types'; import type { GitOperationError } from 'shared/types';
import { repoBranchKeys } from './useRepoBranches';
export function useRebase( export function useRebase(
attemptId: string | undefined, attemptId: string | undefined,
projectId: string | undefined, repoId: string | undefined,
onSuccess?: () => void, onSuccess?: () => void,
onError?: (err: Result<void, GitOperationError>) => void onError?: (err: Result<void, GitOperationError>) => void
) { ) {
@@ -47,10 +48,10 @@ export function useRebase(
queryKey: ['taskAttempt', attemptId], queryKey: ['taskAttempt', attemptId],
}); });
// Refresh branch list used by PR dialog // Refresh branch list
if (projectId) { if (repoId) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['projectBranches', projectId], queryKey: repoBranchKeys.byRepo(repoId),
}); });
} }

View File

@@ -1,8 +1,8 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQueries } from '@tanstack/react-query';
import { useBranches } from './useBranches'; import { repoApi } from '@/lib/api';
import { projectsApi } from '@/lib/api'; import { repoBranchKeys } from './useRepoBranches';
import type { GitBranch, Repo, RepositoryBranches } from 'shared/types'; import type { GitBranch, Repo } from 'shared/types';
export type RepoBranchConfig = { export type RepoBranchConfig = {
repoId: string; repoId: string;
@@ -12,15 +12,13 @@ export type RepoBranchConfig = {
}; };
type UseRepoBranchSelectionOptions = { type UseRepoBranchSelectionOptions = {
projectId: string | undefined; repos: Repo[];
initialBranch?: string | null; initialBranch?: string | null;
enabled?: boolean; enabled?: boolean;
}; };
type UseRepoBranchSelectionReturn = { type UseRepoBranchSelectionReturn = {
configs: RepoBranchConfig[]; configs: RepoBranchConfig[];
repositoryBranches: RepositoryBranches[];
projectRepos: Repo[];
isLoading: boolean; isLoading: boolean;
setRepoBranch: (repoId: string, branch: string) => void; setRepoBranch: (repoId: string, branch: string) => void;
getAttemptRepoInputs: () => Array<{ repo_id: string; target_branch: string }>; getAttemptRepoInputs: () => Array<{ repo_id: string; target_branch: string }>;
@@ -28,7 +26,7 @@ type UseRepoBranchSelectionReturn = {
}; };
export function useRepoBranchSelection({ export function useRepoBranchSelection({
projectId, repos,
initialBranch, initialBranch,
enabled = true, enabled = true,
}: UseRepoBranchSelectionOptions): UseRepoBranchSelectionReturn { }: UseRepoBranchSelectionOptions): UseRepoBranchSelectionReturn {
@@ -36,22 +34,20 @@ export function useRepoBranchSelection({
Record<string, string | null> Record<string, string | null>
>({}); >({});
const { data: repositoryBranches = [], isLoading: isLoadingBranches } = const queries = useQueries({
useBranches(projectId, { enabled: enabled && !!projectId }); queries: repos.map((repo) => ({
queryKey: repoBranchKeys.byRepo(repo.id),
const { data: projectRepos = [], isLoading: isLoadingRepos } = useQuery({ queryFn: () => repoApi.getBranches(repo.id),
queryKey: ['projectRepositories', projectId], enabled,
queryFn: () => staleTime: 60_000,
projectId ? projectsApi.getRepositories(projectId) : Promise.resolve([]), })),
enabled: enabled && !!projectId,
}); });
const isLoadingBranches = queries.some((q) => q.isLoading);
const configs = useMemo((): RepoBranchConfig[] => { const configs = useMemo((): RepoBranchConfig[] => {
return projectRepos.map((repo) => { return repos.map((repo, i) => {
const repoBranchData = repositoryBranches.find( const branches = queries[i]?.data ?? [];
(rb) => rb.repository_id === repo.id
);
const branches = repoBranchData?.branches ?? [];
let targetBranch: string | null = userOverrides[repo.id] ?? null; let targetBranch: string | null = userOverrides[repo.id] ?? null;
@@ -71,7 +67,7 @@ export function useRepoBranchSelection({
branches, branches,
}; };
}); });
}, [projectRepos, repositoryBranches, userOverrides, initialBranch]); }, [repos, queries, userOverrides, initialBranch]);
const setRepoBranch = useCallback((repoId: string, branch: string) => { const setRepoBranch = useCallback((repoId: string, branch: string) => {
setUserOverrides((prev) => ({ setUserOverrides((prev) => ({
@@ -95,9 +91,7 @@ export function useRepoBranchSelection({
return { return {
configs, configs,
repositoryBranches, isLoading: isLoadingBranches,
projectRepos,
isLoading: isLoadingBranches || isLoadingRepos,
setRepoBranch, setRepoBranch,
getAttemptRepoInputs, getAttemptRepoInputs,
reset, reset,

View File

@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { repoApi } from '@/lib/api';
import type { GitBranch } from 'shared/types';
export const repoBranchKeys = {
all: ['repoBranches'] as const,
byRepo: (repoId: string | undefined) => ['repoBranches', repoId] as const,
};
type Options = {
enabled?: boolean;
};
export function useRepoBranches(repoId?: string | null, opts?: Options) {
const enabled = (opts?.enabled ?? true) && !!repoId;
return useQuery<GitBranch[]>({
queryKey: repoBranchKeys.byRepo(repoId ?? undefined),
queryFn: () => repoApi.getBranches(repoId!),
enabled,
staleTime: 60_000,
refetchOnWindowFocus: true,
});
}

View File

@@ -20,8 +20,6 @@ import {
ProjectRepo, ProjectRepo,
Repo, Repo,
RepoWithTargetBranch, RepoWithTargetBranch,
RepositoryBranches,
ProjectBranchesResponse,
CreateProject, CreateProject,
CreateProjectRepo, CreateProjectRepo,
UpdateProjectRepo, UpdateProjectRepo,
@@ -288,12 +286,6 @@ export const projectsApi = {
return handleApiResponse<OpenEditorResponse>(response); return handleApiResponse<OpenEditorResponse>(response);
}, },
getBranches: async (id: string): Promise<RepositoryBranches[]> => {
const response = await makeRequest(`/api/projects/${id}/branches`);
const data = await handleApiResponse<ProjectBranchesResponse>(response);
return data.repositories;
},
searchFiles: async ( searchFiles: async (
id: string, id: string,
query: string, query: string,

View File

@@ -6,12 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { AlertTriangle, Plus, X } from 'lucide-react'; import { AlertTriangle, Plus, X } from 'lucide-react';
import { Loader } from '@/components/ui/loader'; import { Loader } from '@/components/ui/loader';
import { tasksApi } from '@/lib/api'; import { tasksApi } from '@/lib/api';
import type { import type { TaskAttempt, RepoBranchStatus } from 'shared/types';
GitBranch,
TaskAttempt,
RepoBranchStatus,
RepositoryBranches,
} from 'shared/types';
import { openTaskForm } from '@/lib/openTaskForm'; import { openTaskForm } from '@/lib/openTaskForm';
import { FeatureShowcaseDialog } from '@/components/dialogs/global/FeatureShowcaseDialog'; import { FeatureShowcaseDialog } from '@/components/dialogs/global/FeatureShowcaseDialog';
import { showcases } from '@/config/showcases'; import { showcases } from '@/config/showcases';
@@ -24,8 +19,6 @@ import { useTaskAttempts } from '@/hooks/useTaskAttempts';
import { useTaskAttempt } from '@/hooks/useTaskAttempt'; import { useTaskAttempt } from '@/hooks/useTaskAttempt';
import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useBranchStatus, useAttemptExecution } from '@/hooks'; import { useBranchStatus, useAttemptExecution } from '@/hooks';
import { useAttemptRepo } from '@/hooks/useAttemptRepo';
import { projectsApi } from '@/lib/api';
import { paths } from '@/lib/paths'; import { paths } from '@/lib/paths';
import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext';
import { ClickedElementsProvider } from '@/contexts/ClickedElementsProvider'; import { ClickedElementsProvider } from '@/contexts/ClickedElementsProvider';
@@ -108,15 +101,11 @@ function GitErrorBanner() {
function DiffsPanelContainer({ function DiffsPanelContainer({
attempt, attempt,
selectedTask, selectedTask,
projectId,
branchStatus, branchStatus,
branches,
}: { }: {
attempt: TaskAttempt | null; attempt: TaskAttempt | null;
selectedTask: TaskWithAttemptStatus | null; selectedTask: TaskWithAttemptStatus | null;
projectId: string;
branchStatus: RepoBranchStatus[] | null; branchStatus: RepoBranchStatus[] | null;
branches: GitBranch[];
}) { }) {
const { isAttemptRunning } = useAttemptExecution(attempt?.id); const { isAttemptRunning } = useAttemptExecution(attempt?.id);
@@ -127,9 +116,7 @@ function DiffsPanelContainer({
attempt && selectedTask attempt && selectedTask
? { ? {
task: selectedTask, task: selectedTask,
projectId,
branchStatus: branchStatus ?? null, branchStatus: branchStatus ?? null,
branches,
isAttemptRunning, isAttemptRunning,
selectedBranch: branchStatus?.[0]?.target_branch_name ?? null, selectedBranch: branchStatus?.[0]?.target_branch_name ?? null,
} }
@@ -297,23 +284,6 @@ export function ProjectTasks() {
const { data: attempt } = useTaskAttempt(effectiveAttemptId); const { data: attempt } = useTaskAttempt(effectiveAttemptId);
const { data: branchStatus } = useBranchStatus(attempt?.id); const { data: branchStatus } = useBranchStatus(attempt?.id);
const { selectedRepoId } = useAttemptRepo(attempt?.id);
const [repoBranches, setRepoBranches] = useState<RepositoryBranches[]>([]);
useEffect(() => {
if (!projectId) return;
projectsApi
.getBranches(projectId)
.then(setRepoBranches)
.catch(() => setRepoBranches([]));
}, [projectId]);
const branches = useMemo(
() =>
repoBranches.find((r) => r.repository_id === selectedRepoId)?.branches ??
[],
[repoBranches, selectedRepoId]
);
const rawMode = searchParams.get('view') as LayoutMode; const rawMode = searchParams.get('view') as LayoutMode;
const mode: LayoutMode = const mode: LayoutMode =
@@ -1028,9 +998,7 @@ export function ProjectTasks() {
<DiffsPanelContainer <DiffsPanelContainer
attempt={attempt} attempt={attempt}
selectedTask={selectedTask} selectedTask={selectedTask}
projectId={projectId!}
branchStatus={branchStatus ?? null} branchStatus={branchStatus ?? null}
branches={branches}
/> />
)} )}
</div> </div>

View File

@@ -30,7 +30,7 @@ import { CopyFilesField } from '@/components/projects/CopyFilesField';
import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea'; import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea';
import { RepoPickerDialog } from '@/components/dialogs/shared/RepoPickerDialog'; import { RepoPickerDialog } from '@/components/dialogs/shared/RepoPickerDialog';
import { projectsApi } from '@/lib/api'; import { projectsApi } from '@/lib/api';
import { branchKeys } from '@/hooks/useBranches'; import { repoBranchKeys } from '@/hooks/useRepoBranches';
import type { Project, ProjectRepo, Repo, UpdateProject } from 'shared/types'; import type { Project, ProjectRepo, Repo, UpdateProject } from 'shared/types';
interface ProjectFormState { interface ProjectFormState {
@@ -330,7 +330,7 @@ export function ProjectSettings() {
queryKey: ['projectRepositories', selectedProjectId], queryKey: ['projectRepositories', selectedProjectId],
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: branchKeys.byProject(selectedProjectId), queryKey: repoBranchKeys.byRepo(newRepo.id),
}); });
} catch (err) { } catch (err) {
setRepoError( setRepoError(
@@ -353,7 +353,7 @@ export function ProjectSettings() {
queryKey: ['projectRepositories', selectedProjectId], queryKey: ['projectRepositories', selectedProjectId],
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: branchKeys.byProject(selectedProjectId), queryKey: repoBranchKeys.byRepo(repoId),
}); });
} catch (err) { } catch (err) {
setRepoError( setRepoError(

View File

@@ -206,10 +206,6 @@ export type CheckAgentAvailabilityQuery = { executor: BaseCodingAgent, };
export type CurrentUserResponse = { user_id: string, }; export type CurrentUserResponse = { user_id: string, };
export type RepositoryBranches = { repository_id: string, repository_name: string, branches: Array<GitBranch>, };
export type ProjectBranchesResponse = { repositories: Array<RepositoryBranches>, };
export type CreateFollowUpAttempt = { prompt: string, variant: string | null, retry_process_id: string | null, force_when_dirty: boolean | null, perform_git_reset: boolean | null, }; export type CreateFollowUpAttempt = { prompt: string, variant: string | null, retry_process_id: string | null, force_when_dirty: boolean | null, perform_git_reset: boolean | null, };
export type ChangeTargetBranchRequest = { repo_id: string, new_target_branch: string, }; export type ChangeTargetBranchRequest = { repo_id: string, new_target_branch: string, };