Run setup script in parallel with coding agent (vibe-kanban) (#1446)

* The implementation is complete. Here's a summary of all the changes made:

## Summary

### Files Created
- `crates/db/migrations/20251206000000_add_parallel_setup_script_to_projects.sql` - Database migration

### Files Modified

**Backend (Rust):**
- `crates/db/src/models/project.rs` - Added `parallel_setup_script: bool` field to `Project`, `Option<bool>` to `CreateProject`/`UpdateProject`, updated all SQL queries
- `crates/services/src/services/container.rs` - Modified `start_attempt()` to support parallel execution when `parallel_setup_script` is true
- `crates/server/src/routes/projects.rs` - Updated create/update handlers to include new field
- `crates/deployment/src/lib.rs` - Added field to auto-created projects

**Frontend (TypeScript/React):**
- `frontend/src/pages/settings/ProjectSettings.tsx` - Added checkbox UI for parallel setup script setting
- `frontend/src/components/dialogs/projects/ProjectFormDialog.tsx` - Added field to create data
- `frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx` - Added field to update data

**i18n:**
- `frontend/src/i18n/locales/en/settings.json` - Added translation strings
- `frontend/src/i18n/locales/es/settings.json` - Added Spanish translations
- `frontend/src/i18n/locales/ja/settings.json` - Added Japanese translations
- `frontend/src/i18n/locales/ko/settings.json` - Added Korean translations

**SQLx Cache:**
- `crates/db/.sqlx/` - Updated query cache files

### Behavior
- When `parallel_setup_script` is `false` (default): Sequential execution (setup → coding agent → cleanup)
- When `parallel_setup_script` is `true`: Setup script and coding agent start simultaneously, setup script failure does not stop the coding agent

* update useConversationHistory to display multiple running conversation threads at once

* The bug fix is complete. Here's a summary of the changes:

## Bug Fix Summary

**Problem:** When running in parallel mode, if the setup script finishes before the coding agent, the task incorrectly moves to "InReview" status while the coding agent is still running.

**Root Cause:** The `should_finalize()` method returned `true` when `next_action.is_none()`, which is the case for setup scripts in parallel mode. This caused premature task finalization.

**Solution:** Modified `should_finalize()` to check if there are other running non-DevServer processes for the same task attempt before finalizing.

### Files Modified

1. **`crates/db/src/models/execution_process.rs`**
   - Added `has_running_non_dev_server_processes_excluding()` method that accepts an optional `exclude_id` parameter
   - Refactored existing `has_running_non_dev_server_processes()` to delegate to the new method

2. **`crates/services/src/services/container.rs`**
   - Changed `should_finalize()` from sync to async
   - Added check for other running processes before allowing finalization

3. **`crates/local-deployment/src/container.rs`**
   - Updated call site to use `.await` on the now-async `should_finalize()`

* The bug fix is complete. Here's a summary of the changes:

**Fixed files:**

1. **`crates/services/src/services/container.rs`** - Simplified `should_finalize()`:
   - Changed from `async fn` back to `fn` (non-async)
   - Removed the complex database check for other running processes
   - Added a simple check: if the run_reason is `SetupScript` AND `next_action.is_none()` (parallel mode), return `false` (don't finalize)

2. **`crates/local-deployment/src/container.rs`** - Removed `.await` from the `should_finalize()` call

3. **`crates/db/src/models/execution_process.rs`** - Cleaned up by removing the unused `has_running_non_dev_server_processes_excluding()` function and simplifying `has_running_non_dev_server_processes()` back to its original form

**How the fix works:**

- In **parallel mode**: Setup scripts have `next_action: None`, so the new check catches this case and prevents finalization
- In **sequential mode**: Setup scripts have `next_action: Some(coding_agent)`, so they pass this check but won't finalize anyway because `next_action.is_none()` returns `false`
This commit is contained in:
Louis Knight-Webb
2025-12-07 15:25:13 +00:00
committed by GitHub
parent 7da884bc3a
commit 76877ea631
23 changed files with 275 additions and 107 deletions

View File

@@ -51,6 +51,7 @@ const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
dev_script: null,
cleanup_script: null,
copy_files: null,
parallel_setup_script: null,
};
createProject.mutate(createData);
@@ -81,6 +82,7 @@ const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
dev_script: null,
cleanup_script: null,
copy_files: null,
parallel_setup_script: null,
};
createProject.mutate(createData);

View File

@@ -94,6 +94,7 @@ export function NoServerContent({
dev_script: script,
cleanup_script: project.cleanup_script ?? null,
copy_files: project.copy_files ?? null,
parallel_setup_script: project.parallel_setup_script ?? null,
},
},
{

View File

@@ -50,7 +50,7 @@ interface UseConversationHistoryResult {}
const MIN_INITIAL_ENTRIES = 10;
const REMAINING_BATCH_SIZE = 50;
const loadingPatch: PatchTypeWithKey = {
const makeLoadingPatch = (executionProcessId: string): PatchTypeWithKey => ({
type: 'NORMALIZED_ENTRY',
content: {
entry_type: {
@@ -59,9 +59,9 @@ const loadingPatch: PatchTypeWithKey = {
content: '',
timestamp: null,
},
patchKey: 'loading',
executionProcessId: '',
};
patchKey: `${executionProcessId}:loading`,
executionProcessId,
});
const nextActionPatch: (
failed: boolean,
@@ -99,7 +99,7 @@ export const useConversationHistory = ({
const executionProcesses = useRef<ExecutionProcess[]>(executionProcessesRaw);
const displayedExecutionProcesses = useRef<ExecutionProcessStateStore>({});
const loadedInitialEntries = useRef(false);
const lastActiveProcessId = useRef<string | null>(null);
const streamingProcessIdsRef = useRef<Set<string>>(new Set());
const onEntriesUpdatedRef = useRef<OnEntriesUpdated | null>(null);
const mergeIntoDisplayed = (
@@ -191,16 +191,14 @@ export const useConversationHistory = ({
.flatMap((p) => p.entries);
};
const getActiveAgentProcess = (): ExecutionProcess | null => {
const activeProcesses = executionProcesses?.current.filter(
(p) =>
p.status === ExecutionProcessStatus.running &&
p.run_reason !== 'devserver'
const getActiveAgentProcesses = (): ExecutionProcess[] => {
return (
executionProcesses?.current.filter(
(p) =>
p.status === ExecutionProcessStatus.running &&
p.run_reason !== 'devserver'
) ?? []
);
if (activeProcesses.length > 1) {
console.error('More than one active execution process found');
}
return activeProcesses[0] || null;
};
const flattenEntriesForEmit = useCallback(
@@ -312,7 +310,7 @@ export const useConversationHistory = ({
}
if (isProcessRunning && !hasPendingApprovalEntry) {
entries.push(loadingPatch);
entries.push(makeLoadingPatch(p.executionProcess.id));
}
} else if (
p.executionProcess.executor_action.typ.type === 'ScriptRequest'
@@ -625,24 +623,32 @@ export const useConversationHistory = ({
]); // include idListKey so new processes trigger reload
useEffect(() => {
const activeProcess = getActiveAgentProcess();
if (!activeProcess) return;
const activeProcesses = getActiveAgentProcesses();
if (activeProcesses.length === 0) return;
if (!displayedExecutionProcesses.current[activeProcess.id]) {
const runningOrInitial =
Object.keys(displayedExecutionProcesses.current).length > 1
? 'running'
: 'initial';
ensureProcessVisible(activeProcess);
emitEntries(displayedExecutionProcesses.current, runningOrInitial, false);
}
for (const activeProcess of activeProcesses) {
if (!displayedExecutionProcesses.current[activeProcess.id]) {
const runningOrInitial =
Object.keys(displayedExecutionProcesses.current).length > 1
? 'running'
: 'initial';
ensureProcessVisible(activeProcess);
emitEntries(
displayedExecutionProcesses.current,
runningOrInitial,
false
);
}
if (
activeProcess.status === ExecutionProcessStatus.running &&
lastActiveProcessId.current !== activeProcess.id
) {
lastActiveProcessId.current = activeProcess.id;
loadRunningAndEmitWithBackoff(activeProcess);
if (
activeProcess.status === ExecutionProcessStatus.running &&
!streamingProcessIdsRef.current.has(activeProcess.id)
) {
streamingProcessIdsRef.current.add(activeProcess.id);
loadRunningAndEmitWithBackoff(activeProcess).finally(() => {
streamingProcessIdsRef.current.delete(activeProcess.id);
});
}
}
}, [
attempt.id,
@@ -673,7 +679,7 @@ export const useConversationHistory = ({
useEffect(() => {
displayedExecutionProcesses.current = {};
loadedInitialEntries.current = false;
lastActiveProcessId.current = null;
streamingProcessIdsRef.current.clear();
emitEntries(displayedExecutionProcesses.current, 'initial', true);
}, [attempt.id, emitEntries]);

View File

@@ -324,7 +324,9 @@
"description": "Configure setup, development, and cleanup scripts for this project.",
"setup": {
"label": "Setup Script",
"helper": "This script will run after creating the worktree and before the coding agent starts. Use it for setup tasks like installing dependencies or preparing the environment."
"helper": "This script will run after creating the worktree and before the coding agent starts. Use it for setup tasks like installing dependencies or preparing the environment.",
"parallelLabel": "Run setup script in parallel with coding agent",
"parallelHelper": "When enabled, the setup script runs simultaneously with the coding agent instead of waiting for setup to complete first."
},
"dev": {
"label": "Dev Server Script",

View File

@@ -324,7 +324,9 @@
"description": "Configura los scripts de instalación, desarrollo y limpieza para este proyecto.",
"setup": {
"label": "Script de Instalación",
"helper": "Este script se ejecutará después de crear el worktree y antes de que comience el agente de codificación. Úsalo para tareas de configuración como instalar dependencias o preparar el entorno."
"helper": "Este script se ejecutará después de crear el worktree y antes de que comience el agente de codificación. Úsalo para tareas de configuración como instalar dependencias o preparar el entorno.",
"parallelLabel": "Ejecutar script de instalación en paralelo con el agente de codificación",
"parallelHelper": "Cuando está habilitado, el script de instalación se ejecuta simultáneamente con el agente de codificación en lugar de esperar a que se complete la configuración primero."
},
"dev": {
"label": "Script del Servidor de Desarrollo",

View File

@@ -324,7 +324,9 @@
"description": "このプロジェクトのセットアップ、開発、およびクリーンアップスクリプトを設定します。",
"setup": {
"label": "セットアップスクリプト",
"helper": "このスクリプトは、ワークツリーの作成後、コーディングエージェントの開始前に実行されます。依存関係のインストールや環境の準備などのセットアップタスクに使用してください。"
"helper": "このスクリプトは、ワークツリーの作成後、コーディングエージェントの開始前に実行されます。依存関係のインストールや環境の準備などのセットアップタスクに使用してください。",
"parallelLabel": "セットアップスクリプトをコーディングエージェントと並行して実行",
"parallelHelper": "有効にすると、セットアップスクリプトはセットアップの完了を待たずに、コーディングエージェントと同時に実行されます。"
},
"dev": {
"label": "開発サーバースクリプト",

View File

@@ -324,7 +324,9 @@
"description": "이 프로젝트의 설정, 개발 및 정리 스크립트를 구성하세요.",
"setup": {
"label": "설정 스크립트",
"helper": "이 스크립트는 워크트리를 생성한 후 코딩 에이전트가 시작되기 전에 실행됩니다. 종속성 설치 또는 환경 준비와 같은 설정 작업에 사용하세요."
"helper": "이 스크립트는 워크트리를 생성한 후 코딩 에이전트가 시작되기 전에 실행됩니다. 종속성 설치 또는 환경 준비와 같은 설정 작업에 사용하세요.",
"parallelLabel": "설정 스크립트를 코딩 에이전트와 병렬로 실행",
"parallelHelper": "활성화되면 설정 스크립트가 설정 완료를 기다리지 않고 코딩 에이전트와 동시에 실행됩니다."
},
"dev": {
"label": "개발 서버 스크립트",

View File

@@ -19,6 +19,7 @@ import {
} from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, Folder } from 'lucide-react';
import { useProjects } from '@/hooks/useProjects';
@@ -33,6 +34,7 @@ interface ProjectFormState {
name: string;
git_repo_path: string;
setup_script: string;
parallel_setup_script: boolean;
dev_script: string;
cleanup_script: string;
copy_files: string;
@@ -43,6 +45,7 @@ function projectToFormState(project: Project): ProjectFormState {
name: project.name,
git_repo_path: project.git_repo_path,
setup_script: project.setup_script ?? '',
parallel_setup_script: project.parallel_setup_script ?? false,
dev_script: project.dev_script ?? '',
cleanup_script: project.cleanup_script ?? '',
copy_files: project.copy_files ?? '',
@@ -211,6 +214,7 @@ export function ProjectSettings() {
name: draft.name.trim(),
git_repo_path: draft.git_repo_path.trim(),
setup_script: draft.setup_script.trim() || null,
parallel_setup_script: draft.parallel_setup_script,
dev_script: draft.dev_script.trim() || null,
cleanup_script: draft.cleanup_script.trim() || null,
copy_files: draft.copy_files.trim() || null,
@@ -414,6 +418,26 @@ export function ProjectSettings() {
<p className="text-sm text-muted-foreground">
{t('settings.projects.scripts.setup.helper')}
</p>
<div className="flex items-center space-x-2 pt-2">
<Checkbox
id="parallel-setup-script"
checked={draft.parallel_setup_script}
onCheckedChange={(checked) =>
updateDraft({ parallel_setup_script: checked === true })
}
disabled={!draft.setup_script.trim()}
/>
<Label
htmlFor="parallel-setup-script"
className="text-sm font-normal cursor-pointer"
>
{t('settings.projects.scripts.setup.parallelLabel')}
</Label>
</div>
<p className="text-sm text-muted-foreground pl-6">
{t('settings.projects.scripts.setup.parallelHelper')}
</p>
</div>
<div className="space-y-2">