From 76877ea6317644f879cb8cc4b6732ec337b0198d Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Sun, 7 Dec 2025 15:25:13 +0000 Subject: [PATCH] Run setup script in parallel with coding agent (vibe-kanban) (#1446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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` 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` --- ...40a929ad163d4986208bfebb243ed7372199.json} | 16 ++- ...20edec27573bb555a66fbb27cd78161392af.json} | 16 ++- ...5081e7ec23ef0fef7cfe2b60ec9fc9d69d18.json} | 18 ++-- ...20b741ca5e6fac93e71f93a14fb345c6c704.json} | 16 ++- ...ceb7231696a0775ba2f144a0b1ecc8bfd56d.json} | 16 ++- ...46dfce714f1fc57d6625dd752b7aa644b497.json} | 18 ++-- ...a1191a60dacd760f5c711a947b3454735945.json} | 16 ++- ...dba6d739d11567965be2cf3a1ca99beccae2.json} | 16 ++- ..._add_parallel_setup_script_to_projects.sql | 3 + crates/db/src/models/project.rs | 25 ++++- crates/deployment/src/lib.rs | 1 + crates/server/src/routes/projects.rs | 4 + crates/services/src/services/container.rs | 98 ++++++++++++++---- dev_assets_seed/db.sqlite | Bin 0 -> 274432 bytes .../dialogs/projects/ProjectFormDialog.tsx | 2 + .../TaskDetails/preview/NoServerContent.tsx | 1 + frontend/src/hooks/useConversationHistory.ts | 70 +++++++------ frontend/src/i18n/locales/en/settings.json | 4 +- frontend/src/i18n/locales/es/settings.json | 4 +- frontend/src/i18n/locales/ja/settings.json | 4 +- frontend/src/i18n/locales/ko/settings.json | 4 +- .../src/pages/settings/ProjectSettings.tsx | 24 +++++ shared/types.ts | 6 +- 23 files changed, 275 insertions(+), 107 deletions(-) rename crates/db/.sqlx/{query-3c370bbd5b58c1e5de1ca4799c7fe2b3202173a9211c2d1493d79d93493754a3.json => query-1394d6317296aa95af82b78e038e40a929ad163d4986208bfebb243ed7372199.json} (72%) rename crates/db/.sqlx/{query-a6ee0cb1535be5f414429a26c1534afa3f859f87c291b33769049b922ab8ff86.json => query-46139a0b9debe5889e7d35b3cf9b20edec27573bb555a66fbb27cd78161392af.json} (71%) rename crates/db/.sqlx/{query-2d49b016e3d5872a71d07525a9d15637c9799e8918f125058413028ecb931a5c.json => query-59f5b5a5d4ec2a614c039fb84ecf5081e7ec23ef0fef7cfe2b60ec9fc9d69d18.json} (60%) rename crates/db/.sqlx/{query-2330097afa4816aaf7d98e083eac80558ecb9a355384e5076aa744fab27d2f7e.json => query-5ab2baf9bee3fb408b31e8ce408220b741ca5e6fac93e71f93a14fb345c6c704.json} (72%) rename crates/db/.sqlx/{query-24fc0f4f51e4080aebf6131c47eb241ef5c35440b23cfa712311692143be53f3.json => query-5ae74dcee62a835018743862b2beceb7231696a0775ba2f144a0b1ecc8bfd56d.json} (72%) rename crates/db/.sqlx/{query-18a4eb409f5d5ea419c98fabcfaa024126074d3b29202195c6e3b12a75c32338.json => query-921d5710f2e19167d69b60da029246dfce714f1fc57d6625dd752b7aa644b497.json} (61%) rename crates/db/.sqlx/{query-c53e0af00938e45ba437e81cdb6c3e3d5d0ccaf7122c3830d9935dd10111ea70.json => query-cff3d8c29c20cb7ddf3c9ba4c7f8a1191a60dacd760f5c711a947b3454735945.json} (73%) rename crates/db/.sqlx/{query-4c8cc854d7f9ff93fb86a5a1a99cb99c86c50e062281bf3e52e2ebc6537192f0.json => query-e82f7fc1672ff56df544cc59fec2dba6d739d11567965be2cf3a1ca99beccae2.json} (64%) create mode 100644 crates/db/migrations/20251206000000_add_parallel_setup_script_to_projects.sql diff --git a/crates/db/.sqlx/query-3c370bbd5b58c1e5de1ca4799c7fe2b3202173a9211c2d1493d79d93493754a3.json b/crates/db/.sqlx/query-1394d6317296aa95af82b78e038e40a929ad163d4986208bfebb243ed7372199.json similarity index 72% rename from crates/db/.sqlx/query-3c370bbd5b58c1e5de1ca4799c7fe2b3202173a9211c2d1493d79d93493754a3.json rename to crates/db/.sqlx/query-1394d6317296aa95af82b78e038e40a929ad163d4986208bfebb243ed7372199.json index dce637ac..ffec2040 100644 --- a/crates/db/.sqlx/query-3c370bbd5b58c1e5de1ca4799c7fe2b3202173a9211c2d1493d79d93493754a3.json +++ b/crates/db/.sqlx/query-1394d6317296aa95af82b78e038e40a929ad163d4986208bfebb243ed7372199.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE git_repo_path = $1 AND id != $2", + "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE git_repo_path = $1 AND id != $2", "describe": { "columns": [ { @@ -39,18 +39,23 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "3c370bbd5b58c1e5de1ca4799c7fe2b3202173a9211c2d1493d79d93493754a3" + "hash": "1394d6317296aa95af82b78e038e40a929ad163d4986208bfebb243ed7372199" } diff --git a/crates/db/.sqlx/query-a6ee0cb1535be5f414429a26c1534afa3f859f87c291b33769049b922ab8ff86.json b/crates/db/.sqlx/query-46139a0b9debe5889e7d35b3cf9b20edec27573bb555a66fbb27cd78161392af.json similarity index 71% rename from crates/db/.sqlx/query-a6ee0cb1535be5f414429a26c1534afa3f859f87c291b33769049b922ab8ff86.json rename to crates/db/.sqlx/query-46139a0b9debe5889e7d35b3cf9b20edec27573bb555a66fbb27cd78161392af.json index 4d421fa1..0d85083a 100644 --- a/crates/db/.sqlx/query-a6ee0cb1535be5f414429a26c1534afa3f859f87c291b33769049b922ab8ff86.json +++ b/crates/db/.sqlx/query-46139a0b9debe5889e7d35b3cf9b20edec27573bb555a66fbb27cd78161392af.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE id = $1", + "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE remote_project_id = $1\n LIMIT 1", "describe": { "columns": [ { @@ -39,18 +39,23 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "a6ee0cb1535be5f414429a26c1534afa3f859f87c291b33769049b922ab8ff86" + "hash": "46139a0b9debe5889e7d35b3cf9b20edec27573bb555a66fbb27cd78161392af" } diff --git a/crates/db/.sqlx/query-2d49b016e3d5872a71d07525a9d15637c9799e8918f125058413028ecb931a5c.json b/crates/db/.sqlx/query-59f5b5a5d4ec2a614c039fb84ecf5081e7ec23ef0fef7cfe2b60ec9fc9d69d18.json similarity index 60% rename from crates/db/.sqlx/query-2d49b016e3d5872a71d07525a9d15637c9799e8918f125058413028ecb931a5c.json rename to crates/db/.sqlx/query-59f5b5a5d4ec2a614c039fb84ecf5081e7ec23ef0fef7cfe2b60ec9fc9d69d18.json index b115ab27..f58f7787 100644 --- a/crates/db/.sqlx/query-2d49b016e3d5872a71d07525a9d15637c9799e8918f125058413028ecb931a5c.json +++ b/crates/db/.sqlx/query-59f5b5a5d4ec2a614c039fb84ecf5081e7ec23ef0fef7cfe2b60ec9fc9d69d18.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "UPDATE projects\n SET name = $2,\n git_repo_path = $3,\n setup_script = $4,\n dev_script = $5,\n cleanup_script = $6,\n copy_files = $7\n WHERE id = $1\n RETURNING id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"", + "query": "UPDATE projects\n SET name = $2,\n git_repo_path = $3,\n setup_script = $4,\n dev_script = $5,\n cleanup_script = $6,\n copy_files = $7,\n parallel_setup_script = $8\n WHERE id = $1\n RETURNING id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"", "describe": { "columns": [ { @@ -39,23 +39,28 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], "parameters": { - "Right": 7 + "Right": 8 }, "nullable": [ true, @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "2d49b016e3d5872a71d07525a9d15637c9799e8918f125058413028ecb931a5c" + "hash": "59f5b5a5d4ec2a614c039fb84ecf5081e7ec23ef0fef7cfe2b60ec9fc9d69d18" } diff --git a/crates/db/.sqlx/query-2330097afa4816aaf7d98e083eac80558ecb9a355384e5076aa744fab27d2f7e.json b/crates/db/.sqlx/query-5ab2baf9bee3fb408b31e8ce408220b741ca5e6fac93e71f93a14fb345c6c704.json similarity index 72% rename from crates/db/.sqlx/query-2330097afa4816aaf7d98e083eac80558ecb9a355384e5076aa744fab27d2f7e.json rename to crates/db/.sqlx/query-5ab2baf9bee3fb408b31e8ce408220b741ca5e6fac93e71f93a14fb345c6c704.json index 34aab75d..6509e56e 100644 --- a/crates/db/.sqlx/query-2330097afa4816aaf7d98e083eac80558ecb9a355384e5076aa744fab27d2f7e.json +++ b/crates/db/.sqlx/query-5ab2baf9bee3fb408b31e8ce408220b741ca5e6fac93e71f93a14fb345c6c704.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE remote_project_id = $1\n LIMIT 1", + "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE git_repo_path = $1", "describe": { "columns": [ { @@ -39,18 +39,23 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "2330097afa4816aaf7d98e083eac80558ecb9a355384e5076aa744fab27d2f7e" + "hash": "5ab2baf9bee3fb408b31e8ce408220b741ca5e6fac93e71f93a14fb345c6c704" } diff --git a/crates/db/.sqlx/query-24fc0f4f51e4080aebf6131c47eb241ef5c35440b23cfa712311692143be53f3.json b/crates/db/.sqlx/query-5ae74dcee62a835018743862b2beceb7231696a0775ba2f144a0b1ecc8bfd56d.json similarity index 72% rename from crates/db/.sqlx/query-24fc0f4f51e4080aebf6131c47eb241ef5c35440b23cfa712311692143be53f3.json rename to crates/db/.sqlx/query-5ae74dcee62a835018743862b2beceb7231696a0775ba2f144a0b1ecc8bfd56d.json index e4903bd1..7a0a4100 100644 --- a/crates/db/.sqlx/query-24fc0f4f51e4080aebf6131c47eb241ef5c35440b23cfa712311692143be53f3.json +++ b/crates/db/.sqlx/query-5ae74dcee62a835018743862b2beceb7231696a0775ba2f144a0b1ecc8bfd56d.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n ORDER BY created_at DESC", + "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n ORDER BY created_at DESC", "describe": { "columns": [ { @@ -39,18 +39,23 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "24fc0f4f51e4080aebf6131c47eb241ef5c35440b23cfa712311692143be53f3" + "hash": "5ae74dcee62a835018743862b2beceb7231696a0775ba2f144a0b1ecc8bfd56d" } diff --git a/crates/db/.sqlx/query-18a4eb409f5d5ea419c98fabcfaa024126074d3b29202195c6e3b12a75c32338.json b/crates/db/.sqlx/query-921d5710f2e19167d69b60da029246dfce714f1fc57d6625dd752b7aa644b497.json similarity index 61% rename from crates/db/.sqlx/query-18a4eb409f5d5ea419c98fabcfaa024126074d3b29202195c6e3b12a75c32338.json rename to crates/db/.sqlx/query-921d5710f2e19167d69b60da029246dfce714f1fc57d6625dd752b7aa644b497.json index 1d62afc5..d821837b 100644 --- a/crates/db/.sqlx/query-18a4eb409f5d5ea419c98fabcfaa024126074d3b29202195c6e3b12a75c32338.json +++ b/crates/db/.sqlx/query-921d5710f2e19167d69b60da029246dfce714f1fc57d6625dd752b7aa644b497.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO projects (\n id,\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n RETURNING id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"", + "query": "INSERT INTO projects (\n id,\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n RETURNING id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"", "describe": { "columns": [ { @@ -39,23 +39,28 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], "parameters": { - "Right": 7 + "Right": 8 }, "nullable": [ true, @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "18a4eb409f5d5ea419c98fabcfaa024126074d3b29202195c6e3b12a75c32338" + "hash": "921d5710f2e19167d69b60da029246dfce714f1fc57d6625dd752b7aa644b497" } diff --git a/crates/db/.sqlx/query-c53e0af00938e45ba437e81cdb6c3e3d5d0ccaf7122c3830d9935dd10111ea70.json b/crates/db/.sqlx/query-cff3d8c29c20cb7ddf3c9ba4c7f8a1191a60dacd760f5c711a947b3454735945.json similarity index 73% rename from crates/db/.sqlx/query-c53e0af00938e45ba437e81cdb6c3e3d5d0ccaf7122c3830d9935dd10111ea70.json rename to crates/db/.sqlx/query-cff3d8c29c20cb7ddf3c9ba4c7f8a1191a60dacd760f5c711a947b3454735945.json index b19c84c2..4df70f5b 100644 --- a/crates/db/.sqlx/query-c53e0af00938e45ba437e81cdb6c3e3d5d0ccaf7122c3830d9935dd10111ea70.json +++ b/crates/db/.sqlx/query-cff3d8c29c20cb7ddf3c9ba4c7f8a1191a60dacd760f5c711a947b3454735945.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE git_repo_path = $1", + "query": "SELECT id as \"id!: Uuid\",\n name,\n git_repo_path,\n setup_script,\n dev_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n remote_project_id as \"remote_project_id: Uuid\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM projects\n WHERE id = $1", "describe": { "columns": [ { @@ -39,18 +39,23 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "c53e0af00938e45ba437e81cdb6c3e3d5d0ccaf7122c3830d9935dd10111ea70" + "hash": "cff3d8c29c20cb7ddf3c9ba4c7f8a1191a60dacd760f5c711a947b3454735945" } diff --git a/crates/db/.sqlx/query-4c8cc854d7f9ff93fb86a5a1a99cb99c86c50e062281bf3e52e2ebc6537192f0.json b/crates/db/.sqlx/query-e82f7fc1672ff56df544cc59fec2dba6d739d11567965be2cf3a1ca99beccae2.json similarity index 64% rename from crates/db/.sqlx/query-4c8cc854d7f9ff93fb86a5a1a99cb99c86c50e062281bf3e52e2ebc6537192f0.json rename to crates/db/.sqlx/query-e82f7fc1672ff56df544cc59fec2dba6d739d11567965be2cf3a1ca99beccae2.json index ff47ab7c..bcc8dc77 100644 --- a/crates/db/.sqlx/query-4c8cc854d7f9ff93fb86a5a1a99cb99c86c50e062281bf3e52e2ebc6537192f0.json +++ b/crates/db/.sqlx/query-e82f7fc1672ff56df544cc59fec2dba6d739d11567965be2cf3a1ca99beccae2.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT p.id as \"id!: Uuid\", p.name, p.git_repo_path, p.setup_script, p.dev_script, p.cleanup_script, p.copy_files, \n p.remote_project_id as \"remote_project_id: Uuid\",\n p.created_at as \"created_at!: DateTime\", p.updated_at as \"updated_at!: DateTime\"\n FROM projects p\n WHERE p.id IN (\n SELECT DISTINCT t.project_id\n FROM tasks t\n INNER JOIN task_attempts ta ON ta.task_id = t.id\n ORDER BY ta.updated_at DESC\n )\n LIMIT $1\n ", + "query": "\n SELECT p.id as \"id!: Uuid\", p.name, p.git_repo_path, p.setup_script, p.dev_script, p.cleanup_script, p.copy_files,\n p.parallel_setup_script as \"parallel_setup_script!: bool\",\n p.remote_project_id as \"remote_project_id: Uuid\",\n p.created_at as \"created_at!: DateTime\", p.updated_at as \"updated_at!: DateTime\"\n FROM projects p\n WHERE p.id IN (\n SELECT DISTINCT t.project_id\n FROM tasks t\n INNER JOIN task_attempts ta ON ta.task_id = t.id\n ORDER BY ta.updated_at DESC\n )\n LIMIT $1\n ", "describe": { "columns": [ { @@ -39,18 +39,23 @@ "type_info": "Text" }, { - "name": "remote_project_id: Uuid", + "name": "parallel_setup_script!: bool", "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "remote_project_id: Uuid", + "ordinal": 8, "type_info": "Blob" }, { "name": "created_at!: DateTime", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Text" } ], @@ -65,10 +70,11 @@ true, true, true, + false, true, false, false ] }, - "hash": "4c8cc854d7f9ff93fb86a5a1a99cb99c86c50e062281bf3e52e2ebc6537192f0" + "hash": "e82f7fc1672ff56df544cc59fec2dba6d739d11567965be2cf3a1ca99beccae2" } diff --git a/crates/db/migrations/20251206000000_add_parallel_setup_script_to_projects.sql b/crates/db/migrations/20251206000000_add_parallel_setup_script_to_projects.sql new file mode 100644 index 00000000..07a568d4 --- /dev/null +++ b/crates/db/migrations/20251206000000_add_parallel_setup_script_to_projects.sql @@ -0,0 +1,3 @@ +-- Add parallel_setup_script column to projects table +-- When true, setup script runs in parallel with coding agent instead of sequentially +ALTER TABLE projects ADD COLUMN parallel_setup_script INTEGER NOT NULL DEFAULT 0; diff --git a/crates/db/src/models/project.rs b/crates/db/src/models/project.rs index 78b8dda9..4ce20752 100644 --- a/crates/db/src/models/project.rs +++ b/crates/db/src/models/project.rs @@ -30,6 +30,7 @@ pub struct Project { pub dev_script: Option, pub cleanup_script: Option, pub copy_files: Option, + pub parallel_setup_script: bool, pub remote_project_id: Option, #[ts(type = "Date")] pub created_at: DateTime, @@ -46,6 +47,7 @@ pub struct CreateProject { pub dev_script: Option, pub cleanup_script: Option, pub copy_files: Option, + pub parallel_setup_script: Option, } #[derive(Debug, Deserialize, TS)] @@ -56,6 +58,7 @@ pub struct UpdateProject { pub dev_script: Option, pub cleanup_script: Option, pub copy_files: Option, + pub parallel_setup_script: Option, } #[derive(Debug, Serialize, TS)] @@ -89,6 +92,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" @@ -104,7 +108,8 @@ impl Project { sqlx::query_as!( Project, r#" - SELECT p.id as "id!: Uuid", p.name, p.git_repo_path, p.setup_script, p.dev_script, p.cleanup_script, p.copy_files, + SELECT p.id as "id!: Uuid", p.name, p.git_repo_path, p.setup_script, p.dev_script, p.cleanup_script, p.copy_files, + p.parallel_setup_script as "parallel_setup_script!: bool", p.remote_project_id as "remote_project_id: Uuid", p.created_at as "created_at!: DateTime", p.updated_at as "updated_at!: DateTime" FROM projects p @@ -132,6 +137,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" @@ -156,6 +162,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" @@ -181,6 +188,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" @@ -206,6 +214,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" @@ -223,6 +232,7 @@ impl Project { data: &CreateProject, project_id: Uuid, ) -> Result { + let parallel_setup_script = data.parallel_setup_script.unwrap_or(false); sqlx::query_as!( Project, r#"INSERT INTO projects ( @@ -232,9 +242,10 @@ impl Project { setup_script, dev_script, cleanup_script, - copy_files + copy_files, + parallel_setup_script ) VALUES ( - $1, $2, $3, $4, $5, $6, $7 + $1, $2, $3, $4, $5, $6, $7, $8 ) RETURNING id as "id!: Uuid", name, @@ -243,6 +254,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime""#, @@ -253,6 +265,7 @@ impl Project { data.dev_script, data.cleanup_script, data.copy_files, + parallel_setup_script, ) .fetch_one(pool) .await @@ -268,6 +281,7 @@ impl Project { dev_script: Option, cleanup_script: Option, copy_files: Option, + parallel_setup_script: bool, ) -> Result { sqlx::query_as!( Project, @@ -277,7 +291,8 @@ impl Project { setup_script = $4, dev_script = $5, cleanup_script = $6, - copy_files = $7 + copy_files = $7, + parallel_setup_script = $8 WHERE id = $1 RETURNING id as "id!: Uuid", name, @@ -286,6 +301,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script as "parallel_setup_script!: bool", remote_project_id as "remote_project_id: Uuid", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime""#, @@ -296,6 +312,7 @@ impl Project { dev_script, cleanup_script, copy_files, + parallel_setup_script, ) .fetch_one(pool) .await diff --git a/crates/deployment/src/lib.rs b/crates/deployment/src/lib.rs index 1df14453..4ce9953c 100644 --- a/crates/deployment/src/lib.rs +++ b/crates/deployment/src/lib.rs @@ -166,6 +166,7 @@ pub trait Deployment: Clone + Send + Sync + 'static { dev_script: None, cleanup_script: None, copy_files: None, + parallel_setup_script: None, }; // Ensure existing repo has a main branch if it's empty if let Err(e) = self.git().ensure_main_branch_exists(&repo.path) { diff --git a/crates/server/src/routes/projects.rs b/crates/server/src/routes/projects.rs index 6096ba0a..0433fa37 100644 --- a/crates/server/src/routes/projects.rs +++ b/crates/server/src/routes/projects.rs @@ -205,6 +205,7 @@ pub async fn create_project( dev_script, cleanup_script, copy_files, + parallel_setup_script, use_existing_repo, } = payload; tracing::debug!("Creating project '{}'", name); @@ -292,6 +293,7 @@ pub async fn create_project( dev_script, cleanup_script, copy_files, + parallel_setup_script, }, id, ) @@ -333,6 +335,7 @@ pub async fn update_project( dev_script, cleanup_script, copy_files, + parallel_setup_script, } = payload; // If git_repo_path is being changed, check if the new path is already used by another project let git_repo_path = if let Some(new_git_repo_path) = git_repo_path.map(|s| expand_tilde(&s)) @@ -369,6 +372,7 @@ pub async fn update_project( dev_script, cleanup_script, copy_files, + parallel_setup_script.unwrap_or(existing_project.parallel_setup_script), ) .await { diff --git a/crates/services/src/services/container.rs b/crates/services/src/services/container.rs index 9451664b..38f78ccf 100644 --- a/crates/services/src/services/container.rs +++ b/crates/services/src/services/container.rs @@ -123,14 +123,29 @@ pub trait ContainerService { /// A context is finalized when /// - Always when the execution process has failed or been killed /// - Never when the run reason is DevServer + /// - Never when a setup script has no next_action (parallel mode) /// - The next action is None (no follow-up actions) fn should_finalize(&self, ctx: &ExecutionContext) -> bool { + // Never finalize DevServer processes if matches!( ctx.execution_process.run_reason, ExecutionProcessRunReason::DevServer ) { return false; } + + // Never finalize setup scripts without a next_action (parallel mode). + // In sequential mode, setup scripts have next_action pointing to coding agent, + // so they won't finalize anyway (handled by next_action.is_none() check below). + let action = ctx.execution_process.executor_action().unwrap(); + if matches!( + ctx.execution_process.run_reason, + ExecutionProcessRunReason::SetupScript + ) && action.next_action.is_none() + { + return false; + } + // Always finalize failed or killed executions, regardless of next action if matches!( ctx.execution_process.status, @@ -138,12 +153,9 @@ pub trait ContainerService { ) { return true; } + // Otherwise, finalize only if no next action - ctx.execution_process - .executor_action() - .unwrap() - .next_action - .is_none() + action.next_action.is_none() } /// Finalize task execution by updating status to InReview and sending notifications @@ -670,32 +682,74 @@ pub trait ContainerService { let prompt = task.to_prompt(); - let cleanup_action = self.cleanup_action(project.cleanup_script); + let cleanup_action = self.cleanup_action(project.cleanup_script.clone()); // Choose whether to execute the setup_script or coding agent first let execution_process = if let Some(setup_script) = project.setup_script { - let executor_action = ExecutorAction::new( - ExecutorActionType::ScriptRequest(ScriptRequest { - script: setup_script, - language: ScriptRequestLanguage::Bash, - context: ScriptContext::SetupScript, - }), - // once the setup script is done, run the initial coding agent request - Some(Box::new(ExecutorAction::new( + if project.parallel_setup_script { + // PARALLEL EXECUTION: Start setup script and coding agent independently + // Setup script runs without next_action (it completes on its own) + let setup_action = ExecutorAction::new( + ExecutorActionType::ScriptRequest(ScriptRequest { + script: setup_script, + language: ScriptRequestLanguage::Bash, + context: ScriptContext::SetupScript, + }), + None, // No chaining - runs independently + ); + + // Start setup script (ignore errors - coding agent will start regardless) + if let Err(e) = self + .start_execution( + &task_attempt, + &setup_action, + &ExecutionProcessRunReason::SetupScript, + ) + .await + { + tracing::warn!(?e, "Failed to start setup script in parallel mode"); + } + + // Start coding agent independently with cleanup as next_action + let coding_action = ExecutorAction::new( ExecutorActionType::CodingAgentInitialRequest(CodingAgentInitialRequest { prompt, executor_profile_id: executor_profile_id.clone(), }), cleanup_action, - ))), - ); + ); - self.start_execution( - &task_attempt, - &executor_action, - &ExecutionProcessRunReason::SetupScript, - ) - .await? + self.start_execution( + &task_attempt, + &coding_action, + &ExecutionProcessRunReason::CodingAgent, + ) + .await? + } else { + // SEQUENTIAL EXECUTION: Setup script runs first, then coding agent + let executor_action = ExecutorAction::new( + ExecutorActionType::ScriptRequest(ScriptRequest { + script: setup_script, + language: ScriptRequestLanguage::Bash, + context: ScriptContext::SetupScript, + }), + // once the setup script is done, run the initial coding agent request + Some(Box::new(ExecutorAction::new( + ExecutorActionType::CodingAgentInitialRequest(CodingAgentInitialRequest { + prompt, + executor_profile_id: executor_profile_id.clone(), + }), + cleanup_action, + ))), + ); + + self.start_execution( + &task_attempt, + &executor_action, + &ExecutionProcessRunReason::SetupScript, + ) + .await? + } } else { let executor_action = ExecutorAction::new( ExecutorActionType::CodingAgentInitialRequest(CodingAgentInitialRequest { diff --git a/dev_assets_seed/db.sqlite b/dev_assets_seed/db.sqlite index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..85c4744a1961c882ace2baf0bea56b9a021f4961 100644 GIT binary patch literal 274432 zcmeI533wF6w)lIJOeP6g*oS~<0m5V;0g|u;0?3dIA(90q35(m%nXXKl%ybXkJs|-R zf`9_bbw^P^Kre7*5e2~o6cE`&K~Y>TyMlP#Py|`L{=e#FmZalF-+TA}Uhet%8EU$# z>eO=T)Y4sDC8G;i38M;L@X`{>q?%I(12q&OilQb`6qQ3!)KH3|;wg%nO;J=76~qv9 zHp)b*=MXq+dybxf||wDEvpF!fTSanz$OCdp*Z z&otC^VmSuauuP2;*I<`lVtLN#6L=REMJzf+iI)7L&Yk8t4!fnz2S>$4 zNGFOEC30(Bmqe5K_Dn;a{dbch(PEX8mL%-;Nlup0i7@H9(o`>=#u8W0oAo|-yvdxI zX;|Qw=yDI%5wCM8K}}JqZCHU_uMAL196|`O4EY<%u!7=YXp|$r(B>GAM%u@x%Kqvl z3L(W`QE@3MDk~^ZJgK$klxlrgiL%r_t~B5Y<=Tha$_h%6MZ}WdC%OdICt2j^b;%iF z&l!nQ!eY%YLMdUvQ!Or@VL3PL#++nHwJ^9^#6mR|ELN+MDy6bRB{Y&M!Ow9l=l<6z zQe%|Uq6H%9@cKMh!i*)=QbDsG*-^!Mh(k+@i}!kIF0{s!w5P2}`UOr&u_CvLzK+M1Q%6U7;nU zlwu$%5|JXrC1Co$MTp-=k`WZ4lyE~u{gwj&f`zIksGToYkHXZ62R#pk!+#PV%r4cG7-{mw)S8$#k8jpExTm z&ScKcHrzK!HW*<|mv%{PH7l`LY~*sYFvHP^r$&s&^_dRYSGeJ*-$eCw$HH=K5bfozjsWG$!xJ0=2SIMzc8f}V;fa9eFgg+^$K)j zI?6@Nq9kDK^wCmfc!?{O;>Cg+JBev#CDqo-m8I3PVi1F7DBOgGT@Nvz!Rz6b{wJ$E z++_X9NE(_eVu|XRsYcDyfY@krq2+q?q@H$&araJ+Hkr+4!+nJTose}wG&Rrxy}H-0 zd7r>f#4a)<(;baStmM&}VOV)7waZ{-0K)QINL__mqOd@L9?T-~46i#z(6KF8lzrr zRB5cHX!Bt6?C)ug{g{mRkthB*B6fVYWh&W$5&Ls z%}P!I`*Ht2v^2vkqg;JXagwLT>1EvlP1=}P|93=Kb=JSD zR%%!Wl{TD~)NjP~#uY3lN;GL>PSVKp3+*MPw!%>XPl2k+DRVgNMWxPw z7qxQzE;1WM!-|Uw?6#snIJI6Yv8zh-d-eJprRyLwS~AwvlCKSV;;gqsM4L;}8mo=^ zh2cyOUZE(e+hB^KTKA+Vs$~l@H#a0*C3EbaNmmnxC!9!d#b1b@6`$1Xp=KT8*2d+= z9*q@auEs2g>1g_!$r`;rdbsgjqciH;sEWw5k&_}WL{uBD80H&VQOoO1)c>2{JDZI5 z%veuk%c_VKKW|YK0wdnT1UpzVD&;vbqjyH%wDgR$-u+N|#=zd012g;ncINcvaYd@|y`okNeK8f74sk)QN zSfUEN_nb|@a=S05MU0fGB9iBdNaBeRsbD==M{dVgI_-+BxLR6sC2)Jbeh?lKV@C-3QNXSeCl=tJ?DBgU9jAO&2>oJLsjN zsXbGB9C;<`?8{SbG8u1I#Wma5r-H4iU!F*whgY+L#uif#@OaX8-6|`{jy7U!T}H zar*~rP3MnHIqlfKIBTZi+?>J5>#YfId^feD$#|zMed4c=mL3jDCze88EeMl~-UgZsQ2JLac}A7oC?@4fdxcAw*StvkK`eeR1638foGY&PEg{lx{Vj@;iRZPUA3 zR$noik4rb0jK!+(hq*dcy8=?l%7?@w!OtN9(;}}|;8_blJ395Z7i`l$|EehU!KIfk zjb~m<+@IBL-^9)Ljq2XA-8SX* z4*Sov8T;v=j>#KeuZwDLGV-eYSsTX-*vnUAZ6Hh*G(=_TphDoi*O?6G@{X@PRCVkx z?~gakK4_?XYUJLYI8SIbf9|%EV+J05sO8{xQ=T|+@t5a@9@^*L+Hd-sahJdJC7Xt+0Wb+@y>JTb%4b4l{2SKhc`)n3WG@sx%;>gp6Oyl* zUT*RDYweuNCw#Z7c;K3-)Yz(aCgWgLBK|2s&9}PHf&vRII;wS3GX3b7zkc(@j-pHG z?JYCyhs@7kF@5st*Gr^r??1acX5#Y8H@?5*+Qiy58zx4b+vpdgKO9tJHW|li!X8+s z3e%R1^ov$U@`O*1_rEm@fAGz$9e^TWQa9fm$% zvZwb=>&LDynreA6amXD<_Ql-%PR?g*V-m+qZ*4MSC5zX7`Gk_Kvj@|hwhpNpXt}?a zTd|AhL`k4oPSQ!hCzE!re)7W=n}*z%Goxnn!oU3T!H>OqtiGDjjDKkL(voMUH|u%N zQ$G(Ycp>kODO1NKXBQ^EGjLKXlQCaSz`4hj?m$43&-edug{AGA-u*d{)7Wz+j7POx7@@=;T(`IcJOJMr57 z7DE|<_l4WV;A)Ug9y90vec3N7cg7Cs+4`-uzwN)J!>ilhT+n4%+L;FyZ4>V4Gi~m` zS9~q=hHQ8~wWT4m-`+J*ySrAk2sD>#+uY4bA^T_nbCM!f?GE&WP2W$=UUGc-PHN@j zIX_NtINKXyHdaa(p8Qh$Zht=^5r~wz|nf4ej@2zgG_45<2ZL>eA z>QbN|BE7!c?N;G(x*WM^QLmprHax2=p56ZSjy?+#n!BD^)AIP^-!R*s+R@WD|I_@h zK3z8Dn-&)g%gavu)-vynxgC$(7CGZ!Ws=F*U#-Y(l$T_Z!4wx3Uv&7#0qMy;PiFVqKRdCx<@Dtt1JAwEzG#kX z%f;!x?pe}`Ug)kpQO2chX1?OQHT9z&&u^}|ec+W5Wwkq6-TCa^9bcZk zF>C3@npK7m&R#v)d|3O|8=QAj8z!HP64r>#OvXZ0nr*Ax;^jf0YS^x0Y;xntb-R{! zOyD2eQkJo`^xI#S%qy&M?0V>qIn(-W#imc@KlRAge#I?pPjtSVdVp$izGKwmW2eOh zI_j&R?2v`hqR5I8%}L0MMUi&P1B8H@idZBw9KAN-I{)B#=Irt6`D?qj{O2cIve+Ko z4jx&??~f?`@MKN=+;NNFusxF3YwXy?#rgYgzjPojinfWdfjnRR_*h@rQ>* zo}B*9%clMjT#U(>qZIVjkH3|x_4>cjD!uq#O z+b17MKK9#6_bL9v2RnWhUAX3?sclpX)1}Q`lQz(vc&T`pERVs}*JnjD54CYjD0_S7 z@k4ILxvBN9linOReE;rtGj4c(Ywvy$++B-@VxPEo#hiBzZWg=GI`WXQ*OaNzf#K$* zj8`8bWg4hGZM>&k2pNCsnt4WZiK4SQbeOoYdzp*|!<{!A#=NWHZOo`t0@aWNK zyT98VuU~4rOAOulxK}pJx?YxYozNJ$AWf*I@C1DZ}G!{Fe0)T|gp?ZwuRq_WPxmoDb-3@$cfWpdXl-=* zbL`W`ZCA!G-e-LF$tjWAL}ucp=f5=r<8uq*@4Txb!eq=;i#NK@{_D%* z%vtM>MjpO6HLj=Y@H0c_&8&G%=->X_-b>vs&!6<-dnwLskM$k)ihbXtwOL(%Sf9P% zyXHmjjWw8zZ4@~dkNkRyJebfiayg6RDlWe9{pP#czP`1qZ+ZEuPsbc;Rr%hV(}tY> z;+|H6x{TX;JnGPMcctW%XE-*dQxuifIgX;JJ_)<1q>D-aNIH@9QPRGoH(S1-17349;&nOlcC*JXY==mT2-gPd@x0EnocboXHlJpcl7P1 zIzN18XjjD8v{MJ!k8rIx+JWQk~5<_PI%OE{MNTcFGU( zI;zfxxw;zy&KVt4=d6w6+iT8MTa#5M{wblI=6qnCS#^$Gd{OV000000001*HZcDO00000000000Dz_s zy#EgX000000000008Jlw{to~E0000000000O&^&50{{R300000002PK2j>3(00000 z000000MPV-{r>;}0000000000(DVs;{@;S4sAd!;|4ZBe0000000000001-*;Q2oQ z000000000005p9?00000000000MJB%`9A;v0000000000G<{(H4*&oF00000 z0000@ADI6G000000000006^0xA?hgAj9Nz}9!`9eT9;U!m>#h(F*4ji-BxNxrM6)OcBEZJDRBrP#4_Zs#;}6o zVQ7>iztH9wk4D%}P+ zj-PBvMHbOtE@GF(YE>ff`-qP}9Z6g&VsN#VPB{%B$wV#>rnz7~$o*90;(fKw3f6-~ z^@4!Cyo8-<$vas_F6UI_qXpXI!5(K=VdNK;+Vkv=KoJK7_O{N-i8h(dX2bnhu4suC zt3>&aDXfa+OFH$dU%Mg6QhbEht;9+mt@gv}Kq+4aD|N{7oRV8vV?>FT{Gy^WVSxgf zu}D0_TV&7m$QfbJ8HrNVDDsO?isCob!g4-=cMDh)EvXiU=ddN!;-Wbh_INNO-xqK- zizf#vKU9PClktc2=Z{B0su**UlhlZlmL%-;NlGC*?8EI2dr^+PM6NFFrkKJq)>I@` z(gJ3bV4AwfWi`CmVb9MiB5f0;1Z&P38lTp}NmQt~2<6%f?4@>;V=KwA<=W${6lK^< z%*C8+Fc<%K%!PGU=N2Zj#bUTWGSK(~S`(^YVyiG?&^hd0XC1SkHv0xWaW64=A(44E$PRUENoFx@a=7lOrz}U%P50)^4 zh7}hV*lk6D3=6DMe{#ye20VS#Itr)%wW)*kZM! zG5L4pndi^VrGpz=bIW5gF%P1St`uFSqjtDcM_3x_ovlx)e0u~tLf#tW>Q@9)t_+*b> z){B(|#ZZVxbv1{bf}Q2H5>}rZE2vMy6)%;vSQ#2vAxxI`l7XCsk`cCyzWq=IFCYdp zexHYR$;)GMdcvPlX@p0nJ?sBNkBNi);mG2wbGSt-{wD5vc#3Fe_C~6KTtBz4Z?zL}V zK@9uim;G(rR4rVrw<5fo7PbfLhBKXr?e7lenet|B_2tYI8Z46YbP?P zNc4NXv{0+$MNVf}M^#_Z{@}|QIx*_iU0AdYJm5z9)i^8c{|5j7000000001hrVqUT z4*&oF000000000@A9(%`0000000000002!NnEwL+0000000000K+^~2{{R3000000 z0002c^nv+50000000000001<7;QN0700000000000MPV-=l=iz0000000000(DVtJ z|97IO!2d{fx1cDh8AU1H00000000000002^zd}?bWr!qyA|fNH*pT@@0000000000 z0000@6qx@5000000000006^0R=KlZy0000000000(DZ@%KL7v#000000000qePI3% z0000000000002#&1d_$(O+TLi00000000000Pvp-JpTs(0000000000fTj=3{{a91 z000000001>>62irpkk>aD(WKjX4GD)D9M$$D{*K-ZT#g}6!VUp|Dwc`%~r>4h-($Q zK)uo^e1^$vOE%QCU^xcYuuP59iv>3pof0ipIcZ74UZ3P-8Rcqvj>B#%wWIu^T>CiG zxNlTkgp?2|_2RMKZ|ZF_Cnp>J!b)_x2P?jmzrEDhOKrmn>}q(F5{D2%EJOYV8CFm{ z42^Q+7up=-(MbFFRM}TOS&FZs;!;#pR#2dLktI2$S|3&-E47a+4R}F0BkVaNQA&uX z{34WMVORmXBugr?_ymj9s>CEqOiM$#@=YaTRKa2oBcc={5LNI38s$(kVwL?XF}rxL zmz78Yi1_g6v;YmGQc@8XolkIbes4Jz)RM_BDz)d?9ZHNo!RZ$~!7>ZV@(E5+q9wnm z-joYL&3o{koORxZIg)WX;S6!PJiLgRKr#3PMPnEzT`x!49W}d7v62o+gIpXg0n<_- zs8Srk+Q_vJx0Mx?q7;Ufu*7N&?Mtk|_I7%RqJ$LN#G9()0k?igLqiGQT7sOUalF@fNHh z;)iI3dX-d-ddV6LQ|t)3>k0P>is0}VRpqW9t9Ch%6U~EK@1Y6S%}rTBfugaY+CIG4 zVb9MiA`K2TXxPYMA8vQpi*oEGVGU7CVHqni2)XtGd#N4e*h+G2x%N0~kMwBsEy)H- zwmjk_4=Z6O?U#7@w`PXYWyKSFbG=OF!Cee>u>m_JH+I%b6JsU+<7AnTYpD%PRmjai z0Fthd>nU19R*Et`n%%`vXVjIa1PK+_<9EfW{_9IqFIx-KqRl;$4LBf6%~mBXRw~Z! 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 z00000|A&C@{{sL3000000000$(+A%F2LJ#700000004ld56u4o00000000000HEpf F{{WNeNFM+I literal 0 HcmV?d00001 diff --git a/frontend/src/components/dialogs/projects/ProjectFormDialog.tsx b/frontend/src/components/dialogs/projects/ProjectFormDialog.tsx index 25a44bc1..d04ceec8 100644 --- a/frontend/src/components/dialogs/projects/ProjectFormDialog.tsx +++ b/frontend/src/components/dialogs/projects/ProjectFormDialog.tsx @@ -51,6 +51,7 @@ const ProjectFormDialogImpl = NiceModal.create(() => { dev_script: null, cleanup_script: null, copy_files: null, + parallel_setup_script: null, }; createProject.mutate(createData); @@ -81,6 +82,7 @@ const ProjectFormDialogImpl = NiceModal.create(() => { dev_script: null, cleanup_script: null, copy_files: null, + parallel_setup_script: null, }; createProject.mutate(createData); diff --git a/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx b/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx index cb3f8554..64b38a8b 100644 --- a/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx +++ b/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx @@ -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, }, }, { diff --git a/frontend/src/hooks/useConversationHistory.ts b/frontend/src/hooks/useConversationHistory.ts index 23bba636..1ad9b2cb 100644 --- a/frontend/src/hooks/useConversationHistory.ts +++ b/frontend/src/hooks/useConversationHistory.ts @@ -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(executionProcessesRaw); const displayedExecutionProcesses = useRef({}); const loadedInitialEntries = useRef(false); - const lastActiveProcessId = useRef(null); + const streamingProcessIdsRef = useRef>(new Set()); const onEntriesUpdatedRef = useRef(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]); diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index fb4fa46e..7dc7e2ea 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -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", diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json index 21c35041..b7fa2221 100644 --- a/frontend/src/i18n/locales/es/settings.json +++ b/frontend/src/i18n/locales/es/settings.json @@ -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", diff --git a/frontend/src/i18n/locales/ja/settings.json b/frontend/src/i18n/locales/ja/settings.json index 97c264a0..ea7c62b0 100644 --- a/frontend/src/i18n/locales/ja/settings.json +++ b/frontend/src/i18n/locales/ja/settings.json @@ -324,7 +324,9 @@ "description": "このプロジェクトのセットアップ、開発、およびクリーンアップスクリプトを設定します。", "setup": { "label": "セットアップスクリプト", - "helper": "このスクリプトは、ワークツリーの作成後、コーディングエージェントの開始前に実行されます。依存関係のインストールや環境の準備などのセットアップタスクに使用してください。" + "helper": "このスクリプトは、ワークツリーの作成後、コーディングエージェントの開始前に実行されます。依存関係のインストールや環境の準備などのセットアップタスクに使用してください。", + "parallelLabel": "セットアップスクリプトをコーディングエージェントと並行して実行", + "parallelHelper": "有効にすると、セットアップスクリプトはセットアップの完了を待たずに、コーディングエージェントと同時に実行されます。" }, "dev": { "label": "開発サーバースクリプト", diff --git a/frontend/src/i18n/locales/ko/settings.json b/frontend/src/i18n/locales/ko/settings.json index 3fa66277..e785f36d 100644 --- a/frontend/src/i18n/locales/ko/settings.json +++ b/frontend/src/i18n/locales/ko/settings.json @@ -324,7 +324,9 @@ "description": "이 프로젝트의 설정, 개발 및 정리 스크립트를 구성하세요.", "setup": { "label": "설정 스크립트", - "helper": "이 스크립트는 워크트리를 생성한 후 코딩 에이전트가 시작되기 전에 실행됩니다. 종속성 설치 또는 환경 준비와 같은 설정 작업에 사용하세요." + "helper": "이 스크립트는 워크트리를 생성한 후 코딩 에이전트가 시작되기 전에 실행됩니다. 종속성 설치 또는 환경 준비와 같은 설정 작업에 사용하세요.", + "parallelLabel": "설정 스크립트를 코딩 에이전트와 병렬로 실행", + "parallelHelper": "활성화되면 설정 스크립트가 설정 완료를 기다리지 않고 코딩 에이전트와 동시에 실행됩니다." }, "dev": { "label": "개발 서버 스크립트", diff --git a/frontend/src/pages/settings/ProjectSettings.tsx b/frontend/src/pages/settings/ProjectSettings.tsx index bc19b5f0..dfc50374 100644 --- a/frontend/src/pages/settings/ProjectSettings.tsx +++ b/frontend/src/pages/settings/ProjectSettings.tsx @@ -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() {

{t('settings.projects.scripts.setup.helper')}

+ +
+ + updateDraft({ parallel_setup_script: checked === true }) + } + disabled={!draft.setup_script.trim()} + /> + +
+

+ {t('settings.projects.scripts.setup.parallelHelper')} +

diff --git a/shared/types.ts b/shared/types.ts index 951c5af3..e82e35cb 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -12,11 +12,11 @@ export type SharedTask = { id: string, organization_id: string, project_id: stri export type UserData = { user_id: string, first_name: string | null, last_name: string | null, username: string | null, }; -export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, cleanup_script: string | null, copy_files: string | null, remote_project_id: string | null, created_at: Date, updated_at: Date, }; +export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean, remote_project_id: string | null, created_at: Date, updated_at: Date, }; -export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, dev_script: string | null, cleanup_script: string | null, copy_files: string | null, }; +export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, dev_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean | null, }; -export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, cleanup_script: string | null, copy_files: string | null, }; +export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean | null, }; export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };