diff --git a/crates/db/.sqlx/query-04520564f1eaac8491598976ff58b540443f2f1dc35b5a186f243b297089e34b.json b/crates/db/.sqlx/query-04520564f1eaac8491598976ff58b540443f2f1dc35b5a186f243b297089e34b.json
deleted file mode 100644
index ddac89474d..0000000000
--- a/crates/db/.sqlx/query-04520564f1eaac8491598976ff58b540443f2f1dc35b5a186f243b297089e34b.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name, \n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\"\n FROM repos r\n JOIN project_repos pr ON r.id = pr.repo_id\n WHERE pr.project_id = $1\n ORDER BY r.display_name ASC",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "path",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "display_name",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 5,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "04520564f1eaac8491598976ff58b540443f2f1dc35b5a186f243b297089e34b"
-}
diff --git a/crates/db/.sqlx/query-0c9ee0273c78ae47634deb65dba5c052d7d21810b34bcd0bd498e4fc3de0b259.json b/crates/db/.sqlx/query-0c9ee0273c78ae47634deb65dba5c052d7d21810b34bcd0bd498e4fc3de0b259.json
deleted file mode 100644
index 3d5279f246..0000000000
--- a/crates/db/.sqlx/query-0c9ee0273c78ae47634deb65dba5c052d7d21810b34bcd0bd498e4fc3de0b259.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT pr.id as \"id!: Uuid\",\n pr.project_id as \"project_id!: Uuid\",\n pr.repo_id as \"repo_id!: Uuid\",\n r.name as \"repo_name!\",\n pr.setup_script,\n pr.cleanup_script,\n pr.copy_files,\n pr.parallel_setup_script as \"parallel_setup_script!: bool\"\n FROM project_repos pr\n JOIN repos r ON r.id = pr.repo_id\n WHERE pr.project_id = $1\n ORDER BY r.display_name ASC",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "project_id!: Uuid",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "repo_id!: Uuid",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "repo_name!",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "setup_script",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "cleanup_script",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "copy_files",
- "ordinal": 6,
- "type_info": "Text"
- },
- {
- "name": "parallel_setup_script!: bool",
- "ordinal": 7,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- false,
- true,
- true,
- true,
- false
- ]
- },
- "hash": "0c9ee0273c78ae47634deb65dba5c052d7d21810b34bcd0bd498e4fc3de0b259"
-}
diff --git a/crates/db/.sqlx/query-0de4c4f0d2e2eb13dad04fe330b452361ab582a973ca51b2272bd2e1829cfd50.json b/crates/db/.sqlx/query-0de4c4f0d2e2eb13dad04fe330b452361ab582a973ca51b2272bd2e1829cfd50.json
deleted file mode 100644
index 136cc665fd..0000000000
--- a/crates/db/.sqlx/query-0de4c4f0d2e2eb13dad04fe330b452361ab582a973ca51b2272bd2e1829cfd50.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT DISTINCT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\"\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n JOIN workspaces w ON wr.workspace_id = w.id\n WHERE w.task_id = $1\n ORDER BY r.display_name ASC",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "path",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "display_name",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 5,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "0de4c4f0d2e2eb13dad04fe330b452361ab582a973ca51b2272bd2e1829cfd50"
-}
diff --git a/crates/db/.sqlx/query-11c9a90eade69a77abc178d1692426951cf5a6988f17c9fa01cd60cd29d3decf.json b/crates/db/.sqlx/query-11c9a90eade69a77abc178d1692426951cf5a6988f17c9fa01cd60cd29d3decf.json
new file mode 100644
index 0000000000..d9b3f3f277
--- /dev/null
+++ b/crates/db/.sqlx/query-11c9a90eade69a77abc178d1692426951cf5a6988f17c9fa01cd60cd29d3decf.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.setup_script,\n r.cleanup_script,\n r.copy_files,\n r.parallel_setup_script as \"parallel_setup_script!: bool\",\n r.dev_server_script,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\"\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n WHERE wr.workspace_id = $1\n ORDER BY r.display_name ASC",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "11c9a90eade69a77abc178d1692426951cf5a6988f17c9fa01cd60cd29d3decf"
+}
diff --git a/crates/db/.sqlx/query-1a91842cd32944b358169caa306d79bfa1fc5a7141a7b767a3bfb578ea33d215.json b/crates/db/.sqlx/query-1a91842cd32944b358169caa306d79bfa1fc5a7141a7b767a3bfb578ea33d215.json
new file mode 100644
index 0000000000..b07d66ae11
--- /dev/null
+++ b/crates/db/.sqlx/query-1a91842cd32944b358169caa306d79bfa1fc5a7141a7b767a3bfb578ea33d215.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT DISTINCT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.setup_script,\n r.cleanup_script,\n r.copy_files,\n r.parallel_setup_script as \"parallel_setup_script!: bool\",\n r.dev_server_script,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\"\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n JOIN workspaces w ON wr.workspace_id = w.id\n WHERE w.task_id = $1\n ORDER BY r.display_name ASC",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "1a91842cd32944b358169caa306d79bfa1fc5a7141a7b767a3bfb578ea33d215"
+}
diff --git a/crates/db/.sqlx/query-1c513f6c9d9025fa5dda8133a1be2d620301068c177d13b12a776c8c88104e2b.json b/crates/db/.sqlx/query-1c513f6c9d9025fa5dda8133a1be2d620301068c177d13b12a776c8c88104e2b.json
deleted file mode 100644
index ff319618f8..0000000000
--- a/crates/db/.sqlx/query-1c513f6c9d9025fa5dda8133a1be2d620301068c177d13b12a776c8c88104e2b.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "INSERT INTO project_repos (id, project_id, repo_id)\n VALUES ($1, $2, $3)\n RETURNING id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\"",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "project_id!: Uuid",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "repo_id!: Uuid",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "setup_script",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "cleanup_script",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "copy_files",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "parallel_setup_script!: bool",
- "ordinal": 6,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 3
- },
- "nullable": [
- true,
- false,
- false,
- true,
- true,
- true,
- false
- ]
- },
- "hash": "1c513f6c9d9025fa5dda8133a1be2d620301068c177d13b12a776c8c88104e2b"
-}
diff --git a/crates/db/.sqlx/query-ff1ddf3ed4bed81adf8e1d3b8e3c3c8f0fcb9dc51511250f3c32501d8d60f0f0.json b/crates/db/.sqlx/query-1c537a70bcefc0439d1194329ac1e9f3b91bfbfa0d21d377eaaa6bf847320664.json
similarity index 62%
rename from crates/db/.sqlx/query-ff1ddf3ed4bed81adf8e1d3b8e3c3c8f0fcb9dc51511250f3c32501d8d60f0f0.json
rename to crates/db/.sqlx/query-1c537a70bcefc0439d1194329ac1e9f3b91bfbfa0d21d377eaaa6bf847320664.json
index b076836a5e..4cb50e8855 100644
--- a/crates/db/.sqlx/query-ff1ddf3ed4bed81adf8e1d3b8e3c3c8f0fcb9dc51511250f3c32501d8d60f0f0.json
+++ b/crates/db/.sqlx/query-1c537a70bcefc0439d1194329ac1e9f3b91bfbfa0d21d377eaaa6bf847320664.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "INSERT INTO projects (\n id,\n name\n ) VALUES (\n $1, $2\n )\n RETURNING id as \"id!: Uuid\",\n name,\n dev_script,\n dev_script_working_dir,\n default_agent_working_dir,\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 ) VALUES (\n $1, $2\n )\n RETURNING id as \"id!: Uuid\",\n name,\n default_agent_working_dir,\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": [
{
@@ -13,34 +13,24 @@
"ordinal": 1,
"type_info": "Text"
},
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
{
"name": "default_agent_working_dir",
- "ordinal": 4,
+ "ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_project_id: Uuid",
- "ordinal": 5,
+ "ordinal": 3,
"type_info": "Blob"
},
{
"name": "created_at!: DateTime",
- "ordinal": 6,
+ "ordinal": 4,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime",
- "ordinal": 7,
+ "ordinal": 5,
"type_info": "Text"
}
],
@@ -48,8 +38,6 @@
"Right": 2
},
"nullable": [
- true,
- false,
true,
false,
false,
@@ -58,5 +46,5 @@
false
]
},
- "hash": "ff1ddf3ed4bed81adf8e1d3b8e3c3c8f0fcb9dc51511250f3c32501d8d60f0f0"
+ "hash": "1c537a70bcefc0439d1194329ac1e9f3b91bfbfa0d21d377eaaa6bf847320664"
}
diff --git a/crates/db/.sqlx/query-1eae64d51ea1d81c7239fc3640bb15bb59a51333602e7050bec3f5e8fc7fc782.json b/crates/db/.sqlx/query-20ca258823c512b29c3ce0088ec6d1595e13eadce66575987af9970fa4590069.json
similarity index 56%
rename from crates/db/.sqlx/query-1eae64d51ea1d81c7239fc3640bb15bb59a51333602e7050bec3f5e8fc7fc782.json
rename to crates/db/.sqlx/query-20ca258823c512b29c3ce0088ec6d1595e13eadce66575987af9970fa4590069.json
index 263900474a..0ed7303e14 100644
--- a/crates/db/.sqlx/query-1eae64d51ea1d81c7239fc3640bb15bb59a51333602e7050bec3f5e8fc7fc782.json
+++ b/crates/db/.sqlx/query-20ca258823c512b29c3ce0088ec6d1595e13eadce66575987af9970fa4590069.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n name,\n dev_script,\n dev_script_working_dir,\n default_agent_working_dir,\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 default_agent_working_dir,\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 rowid = $1",
"describe": {
"columns": [
{
@@ -13,34 +13,24 @@
"ordinal": 1,
"type_info": "Text"
},
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
{
"name": "default_agent_working_dir",
- "ordinal": 4,
+ "ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_project_id: Uuid",
- "ordinal": 5,
+ "ordinal": 3,
"type_info": "Blob"
},
{
"name": "created_at!: DateTime",
- "ordinal": 6,
+ "ordinal": 4,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime",
- "ordinal": 7,
+ "ordinal": 5,
"type_info": "Text"
}
],
@@ -52,11 +42,9 @@
false,
true,
true,
- true,
- true,
false,
false
]
},
- "hash": "1eae64d51ea1d81c7239fc3640bb15bb59a51333602e7050bec3f5e8fc7fc782"
+ "hash": "20ca258823c512b29c3ce0088ec6d1595e13eadce66575987af9970fa4590069"
}
diff --git a/crates/db/.sqlx/query-23049e8ff32ee491346afb47628d71d1748508dabc033dd2fd059bab8c422630.json b/crates/db/.sqlx/query-23049e8ff32ee491346afb47628d71d1748508dabc033dd2fd059bab8c422630.json
deleted file mode 100644
index 0d5b9832e2..0000000000
--- a/crates/db/.sqlx/query-23049e8ff32ee491346afb47628d71d1748508dabc033dd2fd059bab8c422630.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\"\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n WHERE wr.workspace_id = $1\n ORDER BY r.display_name ASC",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "path",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "display_name",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 5,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "23049e8ff32ee491346afb47628d71d1748508dabc033dd2fd059bab8c422630"
-}
diff --git a/crates/db/.sqlx/query-9cdc6d55c24ff020a6599f59560c7f0d3feacfe64136bb1338bb4c447022f69b.json b/crates/db/.sqlx/query-2c0172d5b2c5bff0914727a57983d5c336f5b2dfa73ca6c2efa4ea23bb526e05.json
similarity index 55%
rename from crates/db/.sqlx/query-9cdc6d55c24ff020a6599f59560c7f0d3feacfe64136bb1338bb4c447022f69b.json
rename to crates/db/.sqlx/query-2c0172d5b2c5bff0914727a57983d5c336f5b2dfa73ca6c2efa4ea23bb526e05.json
index 08cd108b94..75186f56ee 100644
--- a/crates/db/.sqlx/query-9cdc6d55c24ff020a6599f59560c7f0d3feacfe64136bb1338bb4c447022f69b.json
+++ b/crates/db/.sqlx/query-2c0172d5b2c5bff0914727a57983d5c336f5b2dfa73ca6c2efa4ea23bb526e05.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n name,\n dev_script,\n dev_script_working_dir,\n default_agent_working_dir,\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 default_agent_working_dir,\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": [
{
@@ -13,34 +13,24 @@
"ordinal": 1,
"type_info": "Text"
},
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
{
"name": "default_agent_working_dir",
- "ordinal": 4,
+ "ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_project_id: Uuid",
- "ordinal": 5,
+ "ordinal": 3,
"type_info": "Blob"
},
{
"name": "created_at!: DateTime",
- "ordinal": 6,
+ "ordinal": 4,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime",
- "ordinal": 7,
+ "ordinal": 5,
"type_info": "Text"
}
],
@@ -52,11 +42,9 @@
false,
true,
true,
- true,
- true,
false,
false
]
},
- "hash": "9cdc6d55c24ff020a6599f59560c7f0d3feacfe64136bb1338bb4c447022f69b"
+ "hash": "2c0172d5b2c5bff0914727a57983d5c336f5b2dfa73ca6c2efa4ea23bb526e05"
}
diff --git a/crates/db/.sqlx/query-368d9be9608fea5002627625bf8abd3e9073e06bb72fc75f11c2af13f1d43a10.json b/crates/db/.sqlx/query-368d9be9608fea5002627625bf8abd3e9073e06bb72fc75f11c2af13f1d43a10.json
deleted file mode 100644
index 910193cc88..0000000000
--- a/crates/db/.sqlx/query-368d9be9608fea5002627625bf8abd3e9073e06bb72fc75f11c2af13f1d43a10.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n SELECT p.id as \"id!: Uuid\", p.name, p.dev_script, p.dev_script_working_dir,\n p.default_agent_working_dir,\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 workspaces w ON w.task_id = t.id\n ORDER BY w.updated_at DESC\n )\n LIMIT $1\n ",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "default_agent_working_dir",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "remote_project_id: Uuid",
- "ordinal": 5,
- "type_info": "Blob"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 6,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 7,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- true,
- true,
- true,
- true,
- false,
- false
- ]
- },
- "hash": "368d9be9608fea5002627625bf8abd3e9073e06bb72fc75f11c2af13f1d43a10"
-}
diff --git a/crates/db/.sqlx/query-17789c934ca49a04a85505f1a9c861a575ee8fa2e8b6c9e9f4fc177c09caa53d.json b/crates/db/.sqlx/query-46226e272418a9080d4e350da68358dbe7c2a8b70af321e4a026ad985bbbc6fd.json
similarity index 55%
rename from crates/db/.sqlx/query-17789c934ca49a04a85505f1a9c861a575ee8fa2e8b6c9e9f4fc177c09caa53d.json
rename to crates/db/.sqlx/query-46226e272418a9080d4e350da68358dbe7c2a8b70af321e4a026ad985bbbc6fd.json
index 4b5eed086e..52e5f14157 100644
--- a/crates/db/.sqlx/query-17789c934ca49a04a85505f1a9c861a575ee8fa2e8b6c9e9f4fc177c09caa53d.json
+++ b/crates/db/.sqlx/query-46226e272418a9080d4e350da68358dbe7c2a8b70af321e4a026ad985bbbc6fd.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n name,\n dev_script,\n dev_script_working_dir,\n default_agent_working_dir,\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 rowid = $1",
+ "query": "SELECT id as \"id!: Uuid\",\n name,\n default_agent_working_dir,\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": [
{
@@ -13,34 +13,24 @@
"ordinal": 1,
"type_info": "Text"
},
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
{
"name": "default_agent_working_dir",
- "ordinal": 4,
+ "ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_project_id: Uuid",
- "ordinal": 5,
+ "ordinal": 3,
"type_info": "Blob"
},
{
"name": "created_at!: DateTime",
- "ordinal": 6,
+ "ordinal": 4,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime",
- "ordinal": 7,
+ "ordinal": 5,
"type_info": "Text"
}
],
@@ -52,11 +42,9 @@
false,
true,
true,
- true,
- true,
false,
false
]
},
- "hash": "17789c934ca49a04a85505f1a9c861a575ee8fa2e8b6c9e9f4fc177c09caa53d"
+ "hash": "46226e272418a9080d4e350da68358dbe7c2a8b70af321e4a026ad985bbbc6fd"
}
diff --git a/crates/db/.sqlx/query-5ec16f85616366bad8bdcccd9e57d6a7081f974c2f985cd65bfa89b6803edb45.json b/crates/db/.sqlx/query-5ec16f85616366bad8bdcccd9e57d6a7081f974c2f985cd65bfa89b6803edb45.json
new file mode 100644
index 0000000000..8c6fc286aa
--- /dev/null
+++ b/crates/db/.sqlx/query-5ec16f85616366bad8bdcccd9e57d6a7081f974c2f985cd65bfa89b6803edb45.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.setup_script,\n r.cleanup_script,\n r.copy_files,\n r.parallel_setup_script as \"parallel_setup_script!: bool\",\n r.dev_server_script,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\"\n FROM repos r\n JOIN project_repos pr ON r.id = pr.repo_id\n WHERE pr.project_id = $1\n ORDER BY r.display_name ASC",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "5ec16f85616366bad8bdcccd9e57d6a7081f974c2f985cd65bfa89b6803edb45"
+}
diff --git a/crates/db/.sqlx/query-929007c0ea1cd26a8ddc2ee2993285009017dc33e4472bd4bbe7a0adff798f2b.json b/crates/db/.sqlx/query-5ff9809face43fe1f071dfda62b6a30f4a32a9aaace29caf89b95c224482201b.json
similarity index 53%
rename from crates/db/.sqlx/query-929007c0ea1cd26a8ddc2ee2993285009017dc33e4472bd4bbe7a0adff798f2b.json
rename to crates/db/.sqlx/query-5ff9809face43fe1f071dfda62b6a30f4a32a9aaace29caf89b95c224482201b.json
index b5f8868659..908fa82e2d 100644
--- a/crates/db/.sqlx/query-929007c0ea1cd26a8ddc2ee2993285009017dc33e4472bd4bbe7a0adff798f2b.json
+++ b/crates/db/.sqlx/query-5ff9809face43fe1f071dfda62b6a30f4a32a9aaace29caf89b95c224482201b.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "SELECT r.id as \"id!: Uuid\", r.path, r.name, pr.copy_files\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n JOIN workspaces w ON w.id = wr.workspace_id\n JOIN tasks t ON t.id = w.task_id\n LEFT JOIN project_repos pr ON pr.project_id = t.project_id AND pr.repo_id = r.id\n WHERE wr.workspace_id = $1",
+ "query": "SELECT r.id as \"id!: Uuid\", r.path, r.name, r.copy_files\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n WHERE wr.workspace_id = $1",
"describe": {
"columns": [
{
@@ -34,5 +34,5 @@
true
]
},
- "hash": "929007c0ea1cd26a8ddc2ee2993285009017dc33e4472bd4bbe7a0adff798f2b"
+ "hash": "5ff9809face43fe1f071dfda62b6a30f4a32a9aaace29caf89b95c224482201b"
}
diff --git a/crates/db/.sqlx/query-64b5d84eb77fbd8dd733cd92faad07842b1093ec3e8120fefac5c5f7cd7f2f8e.json b/crates/db/.sqlx/query-64b5d84eb77fbd8dd733cd92faad07842b1093ec3e8120fefac5c5f7cd7f2f8e.json
deleted file mode 100644
index a3309d85e8..0000000000
--- a/crates/db/.sqlx/query-64b5d84eb77fbd8dd733cd92faad07842b1093ec3e8120fefac5c5f7cd7f2f8e.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\",\n wr.target_branch\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n WHERE wr.workspace_id = $1\n ORDER BY r.display_name ASC",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "path",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "display_name",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "target_branch",
- "ordinal": 6,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "64b5d84eb77fbd8dd733cd92faad07842b1093ec3e8120fefac5c5f7cd7f2f8e"
-}
diff --git a/crates/db/.sqlx/query-88c15c5c9f2c8edeef4d48c5fcad3607fbc3f7d1402d379d19ad3f4ae2dd0f7d.json b/crates/db/.sqlx/query-668116cde33817d78c0d2425a949c86bb5abfbffd291196bfa9b5c62a8f6e1fd.json
similarity index 54%
rename from crates/db/.sqlx/query-88c15c5c9f2c8edeef4d48c5fcad3607fbc3f7d1402d379d19ad3f4ae2dd0f7d.json
rename to crates/db/.sqlx/query-668116cde33817d78c0d2425a949c86bb5abfbffd291196bfa9b5c62a8f6e1fd.json
index 4099b7f012..16dac509e7 100644
--- a/crates/db/.sqlx/query-88c15c5c9f2c8edeef4d48c5fcad3607fbc3f7d1402d379d19ad3f4ae2dd0f7d.json
+++ b/crates/db/.sqlx/query-668116cde33817d78c0d2425a949c86bb5abfbffd291196bfa9b5c62a8f6e1fd.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n name,\n dev_script,\n dev_script_working_dir,\n default_agent_working_dir,\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 default_agent_working_dir,\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": [
{
@@ -13,34 +13,24 @@
"ordinal": 1,
"type_info": "Text"
},
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
{
"name": "default_agent_working_dir",
- "ordinal": 4,
+ "ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_project_id: Uuid",
- "ordinal": 5,
+ "ordinal": 3,
"type_info": "Blob"
},
{
"name": "created_at!: DateTime",
- "ordinal": 6,
+ "ordinal": 4,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime",
- "ordinal": 7,
+ "ordinal": 5,
"type_info": "Text"
}
],
@@ -52,11 +42,9 @@
false,
true,
true,
- true,
- true,
false,
false
]
},
- "hash": "88c15c5c9f2c8edeef4d48c5fcad3607fbc3f7d1402d379d19ad3f4ae2dd0f7d"
+ "hash": "668116cde33817d78c0d2425a949c86bb5abfbffd291196bfa9b5c62a8f6e1fd"
}
diff --git a/crates/db/.sqlx/query-697001fc14562702ea84061e74bdcc6b9fbef679b8366ab46c1f629a633e9919.json b/crates/db/.sqlx/query-697001fc14562702ea84061e74bdcc6b9fbef679b8366ab46c1f629a633e9919.json
deleted file mode 100644
index b814774124..0000000000
--- a/crates/db/.sqlx/query-697001fc14562702ea84061e74bdcc6b9fbef679b8366ab46c1f629a633e9919.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "UPDATE projects\n SET name = $2, dev_script = $3, dev_script_working_dir = $4, default_agent_working_dir = $5\n WHERE id = $1\n RETURNING id as \"id!: Uuid\",\n name,\n dev_script,\n dev_script_working_dir,\n default_agent_working_dir,\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": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "dev_script",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "dev_script_working_dir",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "default_agent_working_dir",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "remote_project_id: Uuid",
- "ordinal": 5,
- "type_info": "Blob"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 6,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 7,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 5
- },
- "nullable": [
- true,
- false,
- true,
- true,
- true,
- true,
- false,
- false
- ]
- },
- "hash": "697001fc14562702ea84061e74bdcc6b9fbef679b8366ab46c1f629a633e9919"
-}
diff --git a/crates/db/.sqlx/query-74e7bb2262fe70a09d8d2ff43f8ab85f6ae4effcd5e9547ee6f732e6b84c7fe1.json b/crates/db/.sqlx/query-74e7bb2262fe70a09d8d2ff43f8ab85f6ae4effcd5e9547ee6f732e6b84c7fe1.json
new file mode 100644
index 0000000000..a7b01faa40
--- /dev/null
+++ b/crates/db/.sqlx/query-74e7bb2262fe70a09d8d2ff43f8ab85f6ae4effcd5e9547ee6f732e6b84c7fe1.json
@@ -0,0 +1,86 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.setup_script,\n r.cleanup_script,\n r.copy_files,\n r.parallel_setup_script as \"parallel_setup_script!: bool\",\n r.dev_server_script,\n r.created_at as \"created_at!: DateTime\",\n r.updated_at as \"updated_at!: DateTime\",\n wr.target_branch\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n WHERE wr.workspace_id = $1\n ORDER BY r.display_name ASC",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ },
+ {
+ "name": "target_branch",
+ "ordinal": 11,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "74e7bb2262fe70a09d8d2ff43f8ab85f6ae4effcd5e9547ee6f732e6b84c7fe1"
+}
diff --git a/crates/db/.sqlx/query-7b2ab5f95df4b4044595b99352f94aff0fb372fd14d9a24f3e826300d662c33c.json b/crates/db/.sqlx/query-7b2ab5f95df4b4044595b99352f94aff0fb372fd14d9a24f3e826300d662c33c.json
deleted file mode 100644
index efc11b0be1..0000000000
--- a/crates/db/.sqlx/query-7b2ab5f95df4b4044595b99352f94aff0fb372fd14d9a24f3e826300d662c33c.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\"\n FROM project_repos\n WHERE project_id = $1 AND repo_id = $2",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "project_id!: Uuid",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "repo_id!: Uuid",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "setup_script",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "cleanup_script",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "copy_files",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "parallel_setup_script!: bool",
- "ordinal": 6,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 2
- },
- "nullable": [
- true,
- false,
- false,
- true,
- true,
- true,
- false
- ]
- },
- "hash": "7b2ab5f95df4b4044595b99352f94aff0fb372fd14d9a24f3e826300d662c33c"
-}
diff --git a/crates/db/.sqlx/query-7de695b99239b7357d072eccea37dc827fecdad8a7e927b9fbf9a075fd229648.json b/crates/db/.sqlx/query-7de695b99239b7357d072eccea37dc827fecdad8a7e927b9fbf9a075fd229648.json
new file mode 100644
index 0000000000..597492c129
--- /dev/null
+++ b/crates/db/.sqlx/query-7de695b99239b7357d072eccea37dc827fecdad8a7e927b9fbf9a075fd229648.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id as \"id!: Uuid\",\n path,\n name,\n display_name,\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n dev_server_script,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM repos\n WHERE name = '__NEEDS_BACKFILL__'",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "7de695b99239b7357d072eccea37dc827fecdad8a7e927b9fbf9a075fd229648"
+}
diff --git a/crates/db/.sqlx/query-7edace28ea37dd4b02df4025976e5cc5ac1ebd50d183b9d7059b7850d37a8f58.json b/crates/db/.sqlx/query-7edace28ea37dd4b02df4025976e5cc5ac1ebd50d183b9d7059b7850d37a8f58.json
new file mode 100644
index 0000000000..f5c978e255
--- /dev/null
+++ b/crates/db/.sqlx/query-7edace28ea37dd4b02df4025976e5cc5ac1ebd50d183b9d7059b7850d37a8f58.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id as \"id!: Uuid\",\n path,\n name,\n display_name,\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n dev_server_script,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM repos\n ORDER BY display_name ASC",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "7edace28ea37dd4b02df4025976e5cc5ac1ebd50d183b9d7059b7850d37a8f58"
+}
diff --git a/crates/db/.sqlx/query-8348406206f4853743a834ebe9929ee82d10805be336aeac6e914176398afc05.json b/crates/db/.sqlx/query-8348406206f4853743a834ebe9929ee82d10805be336aeac6e914176398afc05.json
new file mode 100644
index 0000000000..48b865e751
--- /dev/null
+++ b/crates/db/.sqlx/query-8348406206f4853743a834ebe9929ee82d10805be336aeac6e914176398afc05.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\"\n FROM project_repos\n WHERE repo_id = $1",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "project_id!: Uuid",
+ "ordinal": 1,
+ "type_info": "Blob"
+ },
+ {
+ "name": "repo_id!: Uuid",
+ "ordinal": 2,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "8348406206f4853743a834ebe9929ee82d10805be336aeac6e914176398afc05"
+}
diff --git a/crates/db/.sqlx/query-522e135173280ab71f7ba25b35efccb3eaeb7dc5fe778595887f70f62aae8df7.json b/crates/db/.sqlx/query-83509bd233fc17609ceca5820f63b7319f0e5f75246a916370efd655db5314e1.json
similarity index 51%
rename from crates/db/.sqlx/query-522e135173280ab71f7ba25b35efccb3eaeb7dc5fe778595887f70f62aae8df7.json
rename to crates/db/.sqlx/query-83509bd233fc17609ceca5820f63b7319f0e5f75246a916370efd655db5314e1.json
index 6a20d5bb28..e875995e7a 100644
--- a/crates/db/.sqlx/query-522e135173280ab71f7ba25b35efccb3eaeb7dc5fe778595887f70f62aae8df7.json
+++ b/crates/db/.sqlx/query-83509bd233fc17609ceca5820f63b7319f0e5f75246a916370efd655db5314e1.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "INSERT INTO repos (id, path, name, display_name)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT(path) DO UPDATE SET updated_at = updated_at\n RETURNING id as \"id!: Uuid\",\n path,\n name,\n display_name,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"",
+ "query": "INSERT INTO repos (id, path, name, display_name)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT(path) DO UPDATE SET updated_at = updated_at\n RETURNING id as \"id!: Uuid\",\n path,\n name,\n display_name,\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n dev_server_script,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"",
"describe": {
"columns": [
{
@@ -24,14 +24,39 @@
"type_info": "Text"
},
{
- "name": "created_at!: DateTime",
+ "name": "setup_script",
"ordinal": 4,
"type_info": "Text"
},
{
- "name": "updated_at!: DateTime",
+ "name": "cleanup_script",
"ordinal": 5,
"type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
}
],
"parameters": {
@@ -42,9 +67,14 @@
false,
false,
false,
+ true,
+ true,
+ true,
+ false,
+ true,
false,
false
]
},
- "hash": "522e135173280ab71f7ba25b35efccb3eaeb7dc5fe778595887f70f62aae8df7"
+ "hash": "83509bd233fc17609ceca5820f63b7319f0e5f75246a916370efd655db5314e1"
}
diff --git a/crates/db/.sqlx/query-89abf784f6f0f38649512169dd4f6d49eb13184deeb91fbd32587343ff0d86ef.json b/crates/db/.sqlx/query-89abf784f6f0f38649512169dd4f6d49eb13184deeb91fbd32587343ff0d86ef.json
new file mode 100644
index 0000000000..3d7b297a99
--- /dev/null
+++ b/crates/db/.sqlx/query-89abf784f6f0f38649512169dd4f6d49eb13184deeb91fbd32587343ff0d86ef.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "INSERT INTO project_repos (id, project_id, repo_id)\n VALUES ($1, $2, $3)\n RETURNING id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\"",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "project_id!: Uuid",
+ "ordinal": 1,
+ "type_info": "Blob"
+ },
+ {
+ "name": "repo_id!: Uuid",
+ "ordinal": 2,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "89abf784f6f0f38649512169dd4f6d49eb13184deeb91fbd32587343ff0d86ef"
+}
diff --git a/crates/db/.sqlx/query-97531ada6561e0e08312b7e54a580d391ec093499d0becd3c9634156d5cfc11d.json b/crates/db/.sqlx/query-97531ada6561e0e08312b7e54a580d391ec093499d0becd3c9634156d5cfc11d.json
new file mode 100644
index 0000000000..6d7cdc8d0a
--- /dev/null
+++ b/crates/db/.sqlx/query-97531ada6561e0e08312b7e54a580d391ec093499d0becd3c9634156d5cfc11d.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\"\n FROM project_repos\n WHERE project_id = $1",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "project_id!: Uuid",
+ "ordinal": 1,
+ "type_info": "Blob"
+ },
+ {
+ "name": "repo_id!: Uuid",
+ "ordinal": 2,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "97531ada6561e0e08312b7e54a580d391ec093499d0becd3c9634156d5cfc11d"
+}
diff --git a/crates/db/.sqlx/query-a235097176aa925424bf705f8a08dfb2e2b118324b9bee1b711a79a916b53810.json b/crates/db/.sqlx/query-a235097176aa925424bf705f8a08dfb2e2b118324b9bee1b711a79a916b53810.json
deleted file mode 100644
index 4306668a90..0000000000
--- a/crates/db/.sqlx/query-a235097176aa925424bf705f8a08dfb2e2b118324b9bee1b711a79a916b53810.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n path,\n name,\n display_name,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM repos\n WHERE id = $1",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "path",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "display_name",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 5,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "a235097176aa925424bf705f8a08dfb2e2b118324b9bee1b711a79a916b53810"
-}
diff --git a/crates/db/.sqlx/query-b2a5ded645013226c6c89950cc228ab7cbd620fb3548bce1ebf51266ecb89334.json b/crates/db/.sqlx/query-b2a5ded645013226c6c89950cc228ab7cbd620fb3548bce1ebf51266ecb89334.json
new file mode 100644
index 0000000000..c3423c348d
--- /dev/null
+++ b/crates/db/.sqlx/query-b2a5ded645013226c6c89950cc228ab7cbd620fb3548bce1ebf51266ecb89334.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE repos\n SET display_name = $1,\n setup_script = $2,\n cleanup_script = $3,\n copy_files = $4,\n parallel_setup_script = $5,\n dev_server_script = $6,\n updated_at = datetime('now', 'subsec')\n WHERE id = $7\n RETURNING id as \"id!: Uuid\",\n path,\n name,\n display_name,\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n dev_server_script,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 7
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "b2a5ded645013226c6c89950cc228ab7cbd620fb3548bce1ebf51266ecb89334"
+}
diff --git a/crates/db/.sqlx/query-b3f813568712e971b44f6cdffa6d136700abaa3fc99417b1107c341899f293af.json b/crates/db/.sqlx/query-b3f813568712e971b44f6cdffa6d136700abaa3fc99417b1107c341899f293af.json
new file mode 100644
index 0000000000..153ce178d8
--- /dev/null
+++ b/crates/db/.sqlx/query-b3f813568712e971b44f6cdffa6d136700abaa3fc99417b1107c341899f293af.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id as \"id!: Uuid\",\n path,\n name,\n display_name,\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\",\n dev_server_script,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM repos\n WHERE id = $1",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "path",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "setup_script",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "cleanup_script",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "copy_files",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "parallel_setup_script!: bool",
+ "ordinal": 7,
+ "type_info": "Integer"
+ },
+ {
+ "name": "dev_server_script",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 10,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "b3f813568712e971b44f6cdffa6d136700abaa3fc99417b1107c341899f293af"
+}
diff --git a/crates/db/.sqlx/query-b5bd7735bd092073e7f643cdbe3644919764a83158e6d8cfb2238ebc13fc9b4b.json b/crates/db/.sqlx/query-b5bd7735bd092073e7f643cdbe3644919764a83158e6d8cfb2238ebc13fc9b4b.json
new file mode 100644
index 0000000000..634c6e7d3b
--- /dev/null
+++ b/crates/db/.sqlx/query-b5bd7735bd092073e7f643cdbe3644919764a83158e6d8cfb2238ebc13fc9b4b.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\"\n FROM project_repos\n WHERE project_id = $1 AND repo_id = $2",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "project_id!: Uuid",
+ "ordinal": 1,
+ "type_info": "Blob"
+ },
+ {
+ "name": "repo_id!: Uuid",
+ "ordinal": 2,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": [
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "b5bd7735bd092073e7f643cdbe3644919764a83158e6d8cfb2238ebc13fc9b4b"
+}
diff --git a/crates/db/.sqlx/query-c00f47bc6ad969ae325c15a5ade865489b8b90309f68101b7ed39a517cf473cb.json b/crates/db/.sqlx/query-c00f47bc6ad969ae325c15a5ade865489b8b90309f68101b7ed39a517cf473cb.json
deleted file mode 100644
index ea1358ef76..0000000000
--- a/crates/db/.sqlx/query-c00f47bc6ad969ae325c15a5ade865489b8b90309f68101b7ed39a517cf473cb.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "UPDATE project_repos\n SET setup_script = $1,\n cleanup_script = $2,\n copy_files = $3,\n parallel_setup_script = $4\n WHERE project_id = $5 AND repo_id = $6\n RETURNING id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\"",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "project_id!: Uuid",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "repo_id!: Uuid",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "setup_script",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "cleanup_script",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "copy_files",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "parallel_setup_script!: bool",
- "ordinal": 6,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 6
- },
- "nullable": [
- true,
- false,
- false,
- true,
- true,
- true,
- false
- ]
- },
- "hash": "c00f47bc6ad969ae325c15a5ade865489b8b90309f68101b7ed39a517cf473cb"
-}
diff --git a/crates/db/.sqlx/query-c6ca04f8ad0f3f918e83a3960b78f1f5215066f9bb46917fc8e50afe86a6da73.json b/crates/db/.sqlx/query-c6ca04f8ad0f3f918e83a3960b78f1f5215066f9bb46917fc8e50afe86a6da73.json
deleted file mode 100644
index 916d224b3c..0000000000
--- a/crates/db/.sqlx/query-c6ca04f8ad0f3f918e83a3960b78f1f5215066f9bb46917fc8e50afe86a6da73.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n path,\n name,\n display_name,\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM repos\n WHERE name = '__NEEDS_BACKFILL__'",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "path",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "display_name",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "created_at!: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "updated_at!: DateTime",
- "ordinal": 5,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 0
- },
- "nullable": [
- true,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "c6ca04f8ad0f3f918e83a3960b78f1f5215066f9bb46917fc8e50afe86a6da73"
-}
diff --git a/crates/db/.sqlx/query-c72348c44cbae9a9605c6d069b97e1391a7fb3600290df9521cf2d8841a3f2b2.json b/crates/db/.sqlx/query-c72348c44cbae9a9605c6d069b97e1391a7fb3600290df9521cf2d8841a3f2b2.json
deleted file mode 100644
index 3c79c826aa..0000000000
--- a/crates/db/.sqlx/query-c72348c44cbae9a9605c6d069b97e1391a7fb3600290df9521cf2d8841a3f2b2.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\"\n FROM project_repos\n WHERE repo_id = $1",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "project_id!: Uuid",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "repo_id!: Uuid",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "setup_script",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "cleanup_script",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "copy_files",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "parallel_setup_script!: bool",
- "ordinal": 6,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- true,
- true,
- true,
- false
- ]
- },
- "hash": "c72348c44cbae9a9605c6d069b97e1391a7fb3600290df9521cf2d8841a3f2b2"
-}
diff --git a/crates/db/.sqlx/query-dcfb1acc0883f660f3300aece1d8e7541bf474fe970664581dfb971345196ee9.json b/crates/db/.sqlx/query-dcfb1acc0883f660f3300aece1d8e7541bf474fe970664581dfb971345196ee9.json
new file mode 100644
index 0000000000..03a3318351
--- /dev/null
+++ b/crates/db/.sqlx/query-dcfb1acc0883f660f3300aece1d8e7541bf474fe970664581dfb971345196ee9.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "\n SELECT p.id as \"id!: Uuid\", p.name,\n p.default_agent_working_dir,\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 workspaces w ON w.task_id = t.id\n ORDER BY w.updated_at DESC\n )\n LIMIT $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "default_agent_working_dir",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "remote_project_id: Uuid",
+ "ordinal": 3,
+ "type_info": "Blob"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 5,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true,
+ false,
+ true,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "dcfb1acc0883f660f3300aece1d8e7541bf474fe970664581dfb971345196ee9"
+}
diff --git a/crates/db/.sqlx/query-e3f01fc53c00ee217b823ca588dc9a5320c9ba6ddf2bd718c817dfbf5fefd21c.json b/crates/db/.sqlx/query-e3f01fc53c00ee217b823ca588dc9a5320c9ba6ddf2bd718c817dfbf5fefd21c.json
new file mode 100644
index 0000000000..5dfa4f8648
--- /dev/null
+++ b/crates/db/.sqlx/query-e3f01fc53c00ee217b823ca588dc9a5320c9ba6ddf2bd718c817dfbf5fefd21c.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE projects\n SET name = $2, default_agent_working_dir = $3\n WHERE id = $1\n RETURNING id as \"id!: Uuid\",\n name,\n default_agent_working_dir,\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": [
+ {
+ "name": "id!: Uuid",
+ "ordinal": 0,
+ "type_info": "Blob"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "default_agent_working_dir",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "remote_project_id: Uuid",
+ "ordinal": 3,
+ "type_info": "Blob"
+ },
+ {
+ "name": "created_at!: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at!: DateTime",
+ "ordinal": 5,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ true,
+ false,
+ true,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "e3f01fc53c00ee217b823ca588dc9a5320c9ba6ddf2bd718c817dfbf5fefd21c"
+}
diff --git a/crates/db/.sqlx/query-eeb190cd03af76eb61174d8b27812f2f3a7b6f4bdafdb298b46aedea19ca5623.json b/crates/db/.sqlx/query-eeb190cd03af76eb61174d8b27812f2f3a7b6f4bdafdb298b46aedea19ca5623.json
deleted file mode 100644
index ae0e581c06..0000000000
--- a/crates/db/.sqlx/query-eeb190cd03af76eb61174d8b27812f2f3a7b6f4bdafdb298b46aedea19ca5623.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT id as \"id!: Uuid\",\n project_id as \"project_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n setup_script,\n cleanup_script,\n copy_files,\n parallel_setup_script as \"parallel_setup_script!: bool\"\n FROM project_repos\n WHERE project_id = $1",
- "describe": {
- "columns": [
- {
- "name": "id!: Uuid",
- "ordinal": 0,
- "type_info": "Blob"
- },
- {
- "name": "project_id!: Uuid",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "repo_id!: Uuid",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "setup_script",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "cleanup_script",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "copy_files",
- "ordinal": 5,
- "type_info": "Text"
- },
- {
- "name": "parallel_setup_script!: bool",
- "ordinal": 6,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- true,
- false,
- false,
- true,
- true,
- true,
- false
- ]
- },
- "hash": "eeb190cd03af76eb61174d8b27812f2f3a7b6f4bdafdb298b46aedea19ca5623"
-}
diff --git a/crates/db/migrations/20260107000000_move_scripts_to_repos.sql b/crates/db/migrations/20260107000000_move_scripts_to_repos.sql
new file mode 100644
index 0000000000..71f186ff35
--- /dev/null
+++ b/crates/db/migrations/20260107000000_move_scripts_to_repos.sql
@@ -0,0 +1,37 @@
+-- Add script columns to repos
+ALTER TABLE repos ADD COLUMN setup_script TEXT;
+ALTER TABLE repos ADD COLUMN cleanup_script TEXT;
+ALTER TABLE repos ADD COLUMN copy_files TEXT;
+ALTER TABLE repos ADD COLUMN parallel_setup_script INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE repos ADD COLUMN dev_server_script TEXT;
+
+-- Migrate from first project_repo (by rowid) for each repo
+UPDATE repos
+SET
+ setup_script = (SELECT pr.setup_script FROM project_repos pr WHERE pr.repo_id = repos.id ORDER BY pr.rowid ASC LIMIT 1),
+ cleanup_script = (SELECT pr.cleanup_script FROM project_repos pr WHERE pr.repo_id = repos.id ORDER BY pr.rowid ASC LIMIT 1),
+ copy_files = (SELECT pr.copy_files FROM project_repos pr WHERE pr.repo_id = repos.id ORDER BY pr.rowid ASC LIMIT 1),
+ parallel_setup_script = COALESCE((SELECT pr.parallel_setup_script FROM project_repos pr WHERE pr.repo_id = repos.id ORDER BY pr.rowid ASC LIMIT 1), 0);
+
+-- Migrate dev_script directly from projects to repos (via first project_repo)
+UPDATE repos
+SET dev_server_script = (
+ SELECT p.dev_script
+ FROM projects p
+ JOIN project_repos pr ON pr.project_id = p.id
+ WHERE pr.repo_id = repos.id
+ AND p.dev_script IS NOT NULL
+ AND p.dev_script != ''
+ ORDER BY pr.rowid ASC
+ LIMIT 1
+);
+
+-- Remove script columns from project_repos
+ALTER TABLE project_repos DROP COLUMN setup_script;
+ALTER TABLE project_repos DROP COLUMN cleanup_script;
+ALTER TABLE project_repos DROP COLUMN copy_files;
+ALTER TABLE project_repos DROP COLUMN parallel_setup_script;
+
+-- Remove dev_script columns from projects
+ALTER TABLE projects DROP COLUMN dev_script;
+ALTER TABLE projects DROP COLUMN dev_script_working_dir;
diff --git a/crates/db/src/models/project.rs b/crates/db/src/models/project.rs
index c8a61e4698..e22086f881 100644
--- a/crates/db/src/models/project.rs
+++ b/crates/db/src/models/project.rs
@@ -21,8 +21,6 @@ pub enum ProjectError {
pub struct Project {
pub id: Uuid,
pub name: String,
- pub dev_script: Option,
- pub dev_script_working_dir: Option,
pub default_agent_working_dir: Option,
pub remote_project_id: Option,
#[ts(type = "Date")]
@@ -40,8 +38,6 @@ pub struct CreateProject {
#[derive(Debug, Deserialize, TS)]
pub struct UpdateProject {
pub name: Option,
- pub dev_script: Option,
- pub dev_script_working_dir: Option,
pub default_agent_working_dir: Option,
}
@@ -74,8 +70,6 @@ impl Project {
Project,
r#"SELECT id as "id!: Uuid",
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
remote_project_id as "remote_project_id: Uuid",
created_at as "created_at!: DateTime",
@@ -92,7 +86,7 @@ impl Project {
sqlx::query_as!(
Project,
r#"
- SELECT p.id as "id!: Uuid", p.name, p.dev_script, p.dev_script_working_dir,
+ SELECT p.id as "id!: Uuid", p.name,
p.default_agent_working_dir,
p.remote_project_id as "remote_project_id: Uuid",
p.created_at as "created_at!: DateTime", p.updated_at as "updated_at!: DateTime"
@@ -116,8 +110,6 @@ impl Project {
Project,
r#"SELECT id as "id!: Uuid",
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
remote_project_id as "remote_project_id: Uuid",
created_at as "created_at!: DateTime",
@@ -135,8 +127,6 @@ impl Project {
Project,
r#"SELECT id as "id!: Uuid",
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
remote_project_id as "remote_project_id: Uuid",
created_at as "created_at!: DateTime",
@@ -157,8 +147,6 @@ impl Project {
Project,
r#"SELECT id as "id!: Uuid",
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
remote_project_id as "remote_project_id: Uuid",
created_at as "created_at!: DateTime",
@@ -187,8 +175,6 @@ impl Project {
)
RETURNING id as "id!: Uuid",
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
remote_project_id as "remote_project_id: Uuid",
created_at as "created_at!: DateTime",
@@ -210,27 +196,21 @@ impl Project {
.ok_or(sqlx::Error::RowNotFound)?;
let name = payload.name.clone().unwrap_or(existing.name);
- let dev_script = payload.dev_script.clone();
- let dev_script_working_dir = payload.dev_script_working_dir.clone();
let default_agent_working_dir = payload.default_agent_working_dir.clone();
sqlx::query_as!(
Project,
r#"UPDATE projects
- SET name = $2, dev_script = $3, dev_script_working_dir = $4, default_agent_working_dir = $5
+ SET name = $2, default_agent_working_dir = $3
WHERE id = $1
RETURNING id as "id!: Uuid",
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
remote_project_id as "remote_project_id: Uuid",
created_at as "created_at!: DateTime",
updated_at as "updated_at!: DateTime""#,
id,
name,
- dev_script,
- dev_script_working_dir,
default_agent_working_dir,
)
.fetch_one(pool)
diff --git a/crates/db/src/models/project_repo.rs b/crates/db/src/models/project_repo.rs
index f1ff36162d..a5ee455779 100644
--- a/crates/db/src/models/project_repo.rs
+++ b/crates/db/src/models/project_repo.rs
@@ -24,23 +24,6 @@ pub struct ProjectRepo {
pub id: Uuid,
pub project_id: Uuid,
pub repo_id: Uuid,
- pub setup_script: Option,
- pub cleanup_script: Option,
- pub copy_files: Option,
- pub parallel_setup_script: bool,
-}
-
-/// ProjectRepo with the associated repo name (for script execution in worktrees)
-#[derive(Debug, Clone, FromRow)]
-pub struct ProjectRepoWithName {
- pub id: Uuid,
- pub project_id: Uuid,
- pub repo_id: Uuid,
- pub repo_name: String,
- pub setup_script: Option,
- pub cleanup_script: Option,
- pub copy_files: Option,
- pub parallel_setup_script: bool,
}
#[derive(Debug, Clone, Deserialize, TS)]
@@ -49,15 +32,6 @@ pub struct CreateProjectRepo {
pub git_repo_path: String,
}
-#[derive(Debug, Clone, Deserialize, TS)]
-#[ts(export)]
-pub struct UpdateProjectRepo {
- pub setup_script: Option,
- pub cleanup_script: Option,
- pub copy_files: Option,
- pub parallel_setup_script: Option,
-}
-
impl ProjectRepo {
pub async fn find_by_project_id(
pool: &SqlitePool,
@@ -67,11 +41,7 @@ impl ProjectRepo {
ProjectRepo,
r#"SELECT id as "id!: Uuid",
project_id as "project_id!: Uuid",
- repo_id as "repo_id!: Uuid",
- setup_script,
- cleanup_script,
- copy_files,
- parallel_setup_script as "parallel_setup_script!: bool"
+ repo_id as "repo_id!: Uuid"
FROM project_repos
WHERE project_id = $1"#,
project_id
@@ -88,11 +58,7 @@ impl ProjectRepo {
ProjectRepo,
r#"SELECT id as "id!: Uuid",
project_id as "project_id!: Uuid",
- repo_id as "repo_id!: Uuid",
- setup_script,
- cleanup_script,
- copy_files,
- parallel_setup_script as "parallel_setup_script!: bool"
+ repo_id as "repo_id!: Uuid"
FROM project_repos
WHERE repo_id = $1"#,
repo_id
@@ -101,30 +67,6 @@ impl ProjectRepo {
.await
}
- pub async fn find_by_project_id_with_names(
- pool: &SqlitePool,
- project_id: Uuid,
- ) -> Result, sqlx::Error> {
- sqlx::query_as!(
- ProjectRepoWithName,
- r#"SELECT pr.id as "id!: Uuid",
- pr.project_id as "project_id!: Uuid",
- pr.repo_id as "repo_id!: Uuid",
- r.name as "repo_name!",
- pr.setup_script,
- pr.cleanup_script,
- pr.copy_files,
- pr.parallel_setup_script as "parallel_setup_script!: bool"
- FROM project_repos pr
- JOIN repos r ON r.id = pr.repo_id
- WHERE pr.project_id = $1
- ORDER BY r.display_name ASC"#,
- project_id
- )
- .fetch_all(pool)
- .await
- }
-
pub async fn find_repos_for_project(
pool: &SqlitePool,
project_id: Uuid,
@@ -134,7 +76,12 @@ impl ProjectRepo {
r#"SELECT r.id as "id!: Uuid",
r.path,
r.name,
- r.display_name,
+ r.display_name,
+ r.setup_script,
+ r.cleanup_script,
+ r.copy_files,
+ r.parallel_setup_script as "parallel_setup_script!: bool",
+ r.dev_server_script,
r.created_at as "created_at!: DateTime",
r.updated_at as "updated_at!: DateTime"
FROM repos r
@@ -156,11 +103,7 @@ impl ProjectRepo {
ProjectRepo,
r#"SELECT id as "id!: Uuid",
project_id as "project_id!: Uuid",
- repo_id as "repo_id!: Uuid",
- setup_script,
- cleanup_script,
- copy_files,
- parallel_setup_script as "parallel_setup_script!: bool"
+ repo_id as "repo_id!: Uuid"
FROM project_repos
WHERE project_id = $1 AND repo_id = $2"#,
project_id,
@@ -231,11 +174,7 @@ impl ProjectRepo {
VALUES ($1, $2, $3)
RETURNING id as "id!: Uuid",
project_id as "project_id!: Uuid",
- repo_id as "repo_id!: Uuid",
- setup_script,
- cleanup_script,
- copy_files,
- parallel_setup_script as "parallel_setup_script!: bool""#,
+ repo_id as "repo_id!: Uuid""#,
id,
project_id,
repo_id
@@ -243,47 +182,4 @@ impl ProjectRepo {
.fetch_one(executor)
.await
}
-
- pub async fn update(
- pool: &SqlitePool,
- project_id: Uuid,
- repo_id: Uuid,
- payload: &UpdateProjectRepo,
- ) -> Result {
- let existing = Self::find_by_project_and_repo(pool, project_id, repo_id).await?;
- let existing = existing.ok_or(ProjectRepoError::NotFound)?;
-
- let setup_script = payload.setup_script.clone();
- let cleanup_script = payload.cleanup_script.clone();
- let copy_files = payload.copy_files.clone();
- let parallel_setup_script = payload
- .parallel_setup_script
- .unwrap_or(existing.parallel_setup_script);
-
- sqlx::query_as!(
- ProjectRepo,
- r#"UPDATE project_repos
- SET setup_script = $1,
- cleanup_script = $2,
- copy_files = $3,
- parallel_setup_script = $4
- WHERE project_id = $5 AND repo_id = $6
- RETURNING id as "id!: Uuid",
- project_id as "project_id!: Uuid",
- repo_id as "repo_id!: Uuid",
- setup_script,
- cleanup_script,
- copy_files,
- parallel_setup_script as "parallel_setup_script!: bool""#,
- setup_script,
- cleanup_script,
- copy_files,
- parallel_setup_script,
- project_id,
- repo_id
- )
- .fetch_one(pool)
- .await
- .map_err(ProjectRepoError::from)
- }
}
diff --git a/crates/db/src/models/repo.rs b/crates/db/src/models/repo.rs
index b39aec4d3d..abeb3517c6 100644
--- a/crates/db/src/models/repo.rs
+++ b/crates/db/src/models/repo.rs
@@ -21,12 +21,28 @@ pub struct Repo {
pub path: PathBuf,
pub name: String,
pub display_name: String,
+ pub setup_script: Option,
+ pub cleanup_script: Option,
+ pub copy_files: Option,
+ pub parallel_setup_script: bool,
+ pub dev_server_script: Option,
#[ts(type = "Date")]
pub created_at: DateTime,
#[ts(type = "Date")]
pub updated_at: DateTime,
}
+#[derive(Debug, Clone, Deserialize, TS)]
+#[ts(export)]
+pub struct UpdateRepo {
+ pub display_name: Option,
+ pub setup_script: Option,
+ pub cleanup_script: Option,
+ pub copy_files: Option,
+ pub parallel_setup_script: Option,
+ pub dev_server_script: Option,
+}
+
impl Repo {
/// Get repos that still have the migration sentinel as their name.
/// Used by the startup backfill to fix repo names.
@@ -37,6 +53,11 @@ impl Repo {
path,
name,
display_name,
+ setup_script,
+ cleanup_script,
+ copy_files,
+ parallel_setup_script as "parallel_setup_script!: bool",
+ dev_server_script,
created_at as "created_at!: DateTime",
updated_at as "updated_at!: DateTime"
FROM repos
@@ -70,6 +91,11 @@ impl Repo {
path,
name,
display_name,
+ setup_script,
+ cleanup_script,
+ copy_files,
+ parallel_setup_script as "parallel_setup_script!: bool",
+ dev_server_script,
created_at as "created_at!: DateTime",
updated_at as "updated_at!: DateTime"
FROM repos
@@ -120,6 +146,11 @@ impl Repo {
path,
name,
display_name,
+ setup_script,
+ cleanup_script,
+ copy_files,
+ parallel_setup_script as "parallel_setup_script!: bool",
+ dev_server_script,
created_at as "created_at!: DateTime",
updated_at as "updated_at!: DateTime""#,
id,
@@ -141,4 +172,81 @@ impl Repo {
.await?;
Ok(result.rows_affected())
}
+
+ pub async fn list_all(pool: &SqlitePool) -> Result, sqlx::Error> {
+ sqlx::query_as!(
+ Repo,
+ r#"SELECT id as "id!: Uuid",
+ path,
+ name,
+ display_name,
+ setup_script,
+ cleanup_script,
+ copy_files,
+ parallel_setup_script as "parallel_setup_script!: bool",
+ dev_server_script,
+ created_at as "created_at!: DateTime",
+ updated_at as "updated_at!: DateTime"
+ FROM repos
+ ORDER BY display_name ASC"#
+ )
+ .fetch_all(pool)
+ .await
+ }
+
+ pub async fn update(
+ pool: &SqlitePool,
+ id: Uuid,
+ payload: &UpdateRepo,
+ ) -> Result {
+ let existing = Self::find_by_id(pool, id)
+ .await?
+ .ok_or(RepoError::NotFound)?;
+
+ let display_name = payload
+ .display_name
+ .clone()
+ .unwrap_or(existing.display_name);
+ let setup_script = payload.setup_script.clone();
+ let cleanup_script = payload.cleanup_script.clone();
+ let copy_files = payload.copy_files.clone();
+ let parallel_setup_script = payload
+ .parallel_setup_script
+ .unwrap_or(existing.parallel_setup_script);
+ let dev_server_script = payload.dev_server_script.clone();
+
+ sqlx::query_as!(
+ Repo,
+ r#"UPDATE repos
+ SET display_name = $1,
+ setup_script = $2,
+ cleanup_script = $3,
+ copy_files = $4,
+ parallel_setup_script = $5,
+ dev_server_script = $6,
+ updated_at = datetime('now', 'subsec')
+ WHERE id = $7
+ RETURNING id as "id!: Uuid",
+ path,
+ name,
+ display_name,
+ setup_script,
+ cleanup_script,
+ copy_files,
+ parallel_setup_script as "parallel_setup_script!: bool",
+ dev_server_script,
+ created_at as "created_at!: DateTime",
+ updated_at as "updated_at!: DateTime""#,
+ display_name,
+ setup_script,
+ cleanup_script,
+ copy_files,
+ parallel_setup_script,
+ dev_server_script,
+ id
+ )
+ .fetch_one(pool)
+ .await
+ .map_err(RepoError::from)
+ }
}
diff --git a/crates/db/src/models/workspace.rs b/crates/db/src/models/workspace.rs
index cf53808d15..a99c67831e 100644
--- a/crates/db/src/models/workspace.rs
+++ b/crates/db/src/models/workspace.rs
@@ -1,6 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
-use sqlx::{FromRow, SqlitePool, Type};
+use sqlx::{FromRow, SqlitePool};
use thiserror::Error;
use ts_rs::TS;
use uuid::Uuid;
@@ -32,18 +32,6 @@ pub struct ContainerInfo {
pub project_id: Uuid,
}
-#[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, TS)]
-#[sqlx(type_name = "workspace_status", rename_all = "lowercase")]
-#[serde(rename_all = "lowercase")]
-pub enum WorkspaceStatus {
- SetupRunning,
- SetupComplete,
- SetupFailed,
- ExecutorRunning,
- ExecutorComplete,
- ExecutorFailed,
-}
-
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
pub struct Workspace {
pub id: Uuid,
diff --git a/crates/db/src/models/workspace_repo.rs b/crates/db/src/models/workspace_repo.rs
index 60a408dfde..4c198f3537 100644
--- a/crates/db/src/models/workspace_repo.rs
+++ b/crates/db/src/models/workspace_repo.rs
@@ -34,7 +34,7 @@ pub struct RepoWithTargetBranch {
pub target_branch: String,
}
-/// Repo info with copy_files configuration from project_repos.
+/// Repo info with copy_files configuration.
#[derive(Debug, Clone)]
pub struct RepoWithCopyFiles {
pub id: Uuid,
@@ -106,6 +106,11 @@ impl WorkspaceRepo {
r.path,
r.name,
r.display_name,
+ r.setup_script,
+ r.cleanup_script,
+ r.copy_files,
+ r.parallel_setup_script as "parallel_setup_script!: bool",
+ r.dev_server_script,
r.created_at as "created_at!: DateTime",
r.updated_at as "updated_at!: DateTime"
FROM repos r
@@ -127,6 +132,11 @@ impl WorkspaceRepo {
r.path,
r.name,
r.display_name,
+ r.setup_script,
+ r.cleanup_script,
+ r.copy_files,
+ r.parallel_setup_script as "parallel_setup_script!: bool",
+ r.dev_server_script,
r.created_at as "created_at!: DateTime",
r.updated_at as "updated_at!: DateTime",
wr.target_branch
@@ -147,6 +157,11 @@ impl WorkspaceRepo {
path: PathBuf::from(row.path),
name: row.name,
display_name: row.display_name,
+ setup_script: row.setup_script,
+ cleanup_script: row.cleanup_script,
+ copy_files: row.copy_files,
+ parallel_setup_script: row.parallel_setup_script,
+ dev_server_script: row.dev_server_script,
created_at: row.created_at,
updated_at: row.updated_at,
},
@@ -228,6 +243,11 @@ impl WorkspaceRepo {
r.path,
r.name,
r.display_name,
+ r.setup_script,
+ r.cleanup_script,
+ r.copy_files,
+ r.parallel_setup_script as "parallel_setup_script!: bool",
+ r.dev_server_script,
r.created_at as "created_at!: DateTime",
r.updated_at as "updated_at!: DateTime"
FROM repos r
@@ -242,18 +262,14 @@ impl WorkspaceRepo {
}
/// Find repos for a workspace with their copy_files configuration.
- /// Uses LEFT JOIN so repos without project_repo entries still appear (with NULL copy_files).
pub async fn find_repos_with_copy_files(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result, sqlx::Error> {
let rows = sqlx::query!(
- r#"SELECT r.id as "id!: Uuid", r.path, r.name, pr.copy_files
+ r#"SELECT r.id as "id!: Uuid", r.path, r.name, r.copy_files
FROM repos r
JOIN workspace_repos wr ON r.id = wr.repo_id
- JOIN workspaces w ON w.id = wr.workspace_id
- JOIN tasks t ON t.id = w.task_id
- LEFT JOIN project_repos pr ON pr.project_id = t.project_id AND pr.repo_id = r.id
WHERE wr.workspace_id = $1"#,
workspace_id
)
diff --git a/crates/local-deployment/src/container.rs b/crates/local-deployment/src/container.rs
index e5d9a449ad..1c92542dd6 100644
--- a/crates/local-deployment/src/container.rs
+++ b/crates/local-deployment/src/container.rs
@@ -18,7 +18,6 @@ use db::{
ExecutionContext, ExecutionProcess, ExecutionProcessRunReason, ExecutionProcessStatus,
},
execution_process_repo_state::ExecutionProcessRepoState,
- project_repo::ProjectRepo,
repo::Repo,
scratch::{DraftFollowUpData, Scratch, ScratchType},
task::{Task, TaskStatus},
@@ -828,9 +827,9 @@ impl LocalContainerService {
)
.await?;
- let project_repos =
- ProjectRepo::find_by_project_id_with_names(&self.db.pool, ctx.project.id).await?;
- let cleanup_action = self.cleanup_actions_for_repos(&project_repos);
+ let repos =
+ WorkspaceRepo::find_repos_for_workspace(&self.db.pool, ctx.workspace.id).await?;
+ let cleanup_action = self.cleanup_actions_for_repos(&repos);
let working_dir = ctx
.workspace
diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs
index 1e977dddcd..50f00dace8 100644
--- a/crates/server/src/bin/generate_types.rs
+++ b/crates/server/src/bin/generate_types.rs
@@ -21,9 +21,9 @@ fn generate_types_content() -> String {
db::models::project::SearchResult::decl(),
db::models::project::SearchMatchType::decl(),
db::models::repo::Repo::decl(),
+ db::models::repo::UpdateRepo::decl(),
db::models::project_repo::ProjectRepo::decl(),
db::models::project_repo::CreateProjectRepo::decl(),
- db::models::project_repo::UpdateProjectRepo::decl(),
db::models::workspace_repo::WorkspaceRepo::decl(),
db::models::workspace_repo::CreateWorkspaceRepo::decl(),
db::models::workspace_repo::RepoWithTargetBranch::decl(),
@@ -134,7 +134,6 @@ fn generate_types_content() -> String {
server::routes::task_attempts::pr::PrError::decl(),
server::routes::task_attempts::BranchStatus::decl(),
server::routes::task_attempts::RunScriptError::decl(),
- server::routes::task_attempts::DeleteWorkspaceError::decl(),
server::routes::task_attempts::pr::AttachPrResponse::decl(),
server::routes::task_attempts::pr::AttachExistingPrRequest::decl(),
server::routes::task_attempts::pr::PrCommentsResponse::decl(),
diff --git a/crates/server/src/routes/projects.rs b/crates/server/src/routes/projects.rs
index b5c856629d..c08424db54 100644
--- a/crates/server/src/routes/projects.rs
+++ b/crates/server/src/routes/projects.rs
@@ -14,7 +14,7 @@ use axum::{
};
use db::models::{
project::{CreateProject, Project, ProjectError, SearchResult, UpdateProject},
- project_repo::{CreateProjectRepo, ProjectRepo, UpdateProjectRepo},
+ project_repo::{CreateProjectRepo, ProjectRepo},
repo::Repo,
};
use deployment::Deployment;
@@ -568,20 +568,6 @@ pub async fn get_project_repository(
}
}
-pub async fn update_project_repository(
- State(deployment): State,
- Path((project_id, repo_id)): Path<(Uuid, Uuid)>,
- Json(payload): Json,
-) -> Result>, ApiError> {
- match ProjectRepo::update(&deployment.db().pool, project_id, repo_id, &payload).await {
- Ok(project_repo) => Ok(ResponseJson(ApiResponse::success(project_repo))),
- Err(db::models::project_repo::ProjectRepoError::NotFound) => Err(ApiError::BadRequest(
- "Repository not found in project".to_string(),
- )),
- Err(e) => Err(e.into()),
- }
-}
-
pub fn router(deployment: &DeploymentImpl) -> Router {
let project_id_router = Router::new()
.route(
@@ -609,9 +595,7 @@ pub fn router(deployment: &DeploymentImpl) -> Router {
.route("/", get(get_projects).post(create_project))
.route(
"/{project_id}/repositories/{repo_id}",
- get(get_project_repository)
- .put(update_project_repository)
- .delete(delete_project_repository),
+ get(get_project_repository).delete(delete_project_repository),
)
.route("/stream/ws", get(stream_projects_ws))
.nest("/{id}", project_id_router);
diff --git a/crates/server/src/routes/repo.rs b/crates/server/src/routes/repo.rs
index 40e96b3230..476dcb9f21 100644
--- a/crates/server/src/routes/repo.rs
+++ b/crates/server/src/routes/repo.rs
@@ -4,7 +4,7 @@ use axum::{
response::Json as ResponseJson,
routing::{get, post},
};
-use db::models::repo::Repo;
+use db::models::repo::{Repo, UpdateRepo};
use deployment::Deployment;
use serde::Deserialize;
use services::services::git::GitBranch;
@@ -92,6 +92,33 @@ pub async fn get_repos_batch(
Ok(ResponseJson(ApiResponse::success(repos)))
}
+pub async fn get_repos(
+ State(deployment): State,
+) -> Result>>, ApiError> {
+ let repos = Repo::list_all(&deployment.db().pool).await?;
+ Ok(ResponseJson(ApiResponse::success(repos)))
+}
+
+pub async fn get_repo(
+ State(deployment): State,
+ Path(repo_id): Path,
+) -> Result>, ApiError> {
+ let repo = deployment
+ .repo()
+ .get_by_id(&deployment.db().pool, repo_id)
+ .await?;
+ Ok(ResponseJson(ApiResponse::success(repo)))
+}
+
+pub async fn update_repo(
+ State(deployment): State,
+ Path(repo_id): Path,
+ ResponseJson(payload): ResponseJson,
+) -> Result>, ApiError> {
+ let repo = Repo::update(&deployment.db().pool, repo_id, &payload).await?;
+ Ok(ResponseJson(ApiResponse::success(repo)))
+}
+
pub async fn open_repo_in_editor(
State(deployment): State,
Path(repo_id): Path,
@@ -141,9 +168,10 @@ pub async fn open_repo_in_editor(
pub fn router() -> Router {
Router::new()
- .route("/repos", post(register_repo))
+ .route("/repos", get(get_repos).post(register_repo))
.route("/repos/init", post(init_repo))
.route("/repos/batch", post(get_repos_batch))
+ .route("/repos/{repo_id}", get(get_repo).put(update_repo))
.route("/repos/{repo_id}/branches", get(get_repo_branches))
.route("/repos/{repo_id}/open-editor", post(open_repo_in_editor))
}
diff --git a/crates/server/src/routes/sessions/mod.rs b/crates/server/src/routes/sessions/mod.rs
index 83547e2880..ccf689b4db 100644
--- a/crates/server/src/routes/sessions/mod.rs
+++ b/crates/server/src/routes/sessions/mod.rs
@@ -11,10 +11,10 @@ use axum::{
};
use db::models::{
execution_process::{ExecutionProcess, ExecutionProcessRunReason},
- project_repo::ProjectRepo,
scratch::{Scratch, ScratchType},
session::{CreateSession, Session},
workspace::{Workspace, WorkspaceError},
+ workspace_repo::WorkspaceRepo,
};
use deployment::Deployment;
use executors::{
@@ -26,7 +26,6 @@ use executors::{
};
use serde::Deserialize;
use services::services::container::ContainerService;
-use sqlx::Error as SqlxError;
use ts_rs::TS;
use utils::response::ApiResponse;
use uuid::Uuid;
@@ -144,18 +143,6 @@ pub async fn follow_up(
variant: payload.variant,
};
- // Get parent task
- let task = workspace
- .parent_task(pool)
- .await?
- .ok_or(SqlxError::RowNotFound)?;
-
- // Get parent project
- let project = task
- .parent_project(pool)
- .await?
- .ok_or(SqlxError::RowNotFound)?;
-
// If retry settings provided, perform replace-logic before proceeding
if let Some(proc_id) = payload.retry_process_id {
// Validate process belongs to this session
@@ -196,10 +183,8 @@ pub async fn follow_up(
let prompt = payload.prompt;
- let project_repos = ProjectRepo::find_by_project_id_with_names(pool, project.id).await?;
- let cleanup_action = deployment
- .container()
- .cleanup_actions_for_repos(&project_repos);
+ let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
+ let cleanup_action = deployment.container().cleanup_actions_for_repos(&repos);
let working_dir = workspace
.agent_working_dir
diff --git a/crates/server/src/routes/task_attempts.rs b/crates/server/src/routes/task_attempts.rs
index 6c68ad843c..9a2811dbd5 100644
--- a/crates/server/src/routes/task_attempts.rs
+++ b/crates/server/src/routes/task_attempts.rs
@@ -26,7 +26,6 @@ use db::models::{
coding_agent_turn::CodingAgentTurn,
execution_process::{ExecutionProcess, ExecutionProcessRunReason, ExecutionProcessStatus},
merge::{Merge, MergeStatus, PrMerge, PullRequestInfo},
- project_repo::ProjectRepo,
repo::{Repo, RepoError},
session::{CreateSession, Session},
task::{Task, TaskRelationships, TaskStatus},
@@ -1230,33 +1229,18 @@ pub async fn start_dev_server(
}
}
- // Get dev script from project (dev_script is project-level, not per-repo)
- let dev_script = match &project.dev_script {
- Some(script) if !script.is_empty() => script.clone(),
- _ => {
- return Ok(ResponseJson(ApiResponse::error(
- "No dev server script configured for this project",
- )));
- }
- };
-
- let working_dir = project
- .dev_script_working_dir
- .as_ref()
- .filter(|dir| !dir.is_empty())
- .cloned();
+ let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
+ let repos_with_dev_script: Vec<_> = repos
+ .iter()
+ .filter(|r| r.dev_server_script.as_ref().is_some_and(|s| !s.is_empty()))
+ .collect();
- let executor_action = ExecutorAction::new(
- ExecutorActionType::ScriptRequest(ScriptRequest {
- script: dev_script,
- language: ScriptRequestLanguage::Bash,
- context: ScriptContext::DevServer,
- working_dir,
- }),
- None,
- );
+ if repos_with_dev_script.is_empty() {
+ return Ok(ResponseJson(ApiResponse::error(
+ "No dev server script configured for any repository in this workspace",
+ )));
+ }
- // Get or create a session for dev server
let session = match Session::find_latest_by_workspace_id(pool, workspace.id).await? {
Some(s) => s,
None => {
@@ -1272,15 +1256,27 @@ pub async fn start_dev_server(
}
};
- deployment
- .container()
- .start_execution(
- &workspace,
- &session,
- &executor_action,
- &ExecutionProcessRunReason::DevServer,
- )
- .await?;
+ for repo in repos_with_dev_script {
+ let executor_action = ExecutorAction::new(
+ ExecutorActionType::ScriptRequest(ScriptRequest {
+ script: repo.dev_server_script.clone().unwrap(),
+ language: ScriptRequestLanguage::Bash,
+ context: ScriptContext::DevServer,
+ working_dir: Some(repo.name.clone()),
+ }),
+ None,
+ );
+
+ deployment
+ .container()
+ .start_execution(
+ &workspace,
+ &session,
+ &executor_action,
+ &ExecutionProcessRunReason::DevServer,
+ )
+ .await?;
+ }
deployment
.track_if_analytics_allowed(
@@ -1373,7 +1369,6 @@ pub async fn run_setup_script(
.ensure_container_exists(&workspace)
.await?;
- // Get parent task and project
let task = workspace
.parent_task(pool)
.await?
@@ -1383,11 +1378,9 @@ pub async fn run_setup_script(
.parent_project(pool)
.await?
.ok_or(SqlxError::RowNotFound)?;
- let project_repos = ProjectRepo::find_by_project_id_with_names(pool, project.id).await?;
- let executor_action = match deployment
- .container()
- .setup_actions_for_repos(&project_repos)
- {
+
+ let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
+ let executor_action = match deployment.container().setup_actions_for_repos(&repos) {
Some(action) => action,
None => {
return Ok(ResponseJson(ApiResponse::error_with_data(
@@ -1457,7 +1450,6 @@ pub async fn run_cleanup_script(
.ensure_container_exists(&workspace)
.await?;
- // Get parent task and project
let task = workspace
.parent_task(pool)
.await?
@@ -1467,11 +1459,9 @@ pub async fn run_cleanup_script(
.parent_project(pool)
.await?
.ok_or(SqlxError::RowNotFound)?;
- let project_repos = ProjectRepo::find_by_project_id_with_names(pool, project.id).await?;
- let executor_action = match deployment
- .container()
- .cleanup_actions_for_repos(&project_repos)
- {
+
+ let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
+ let executor_action = match deployment.container().cleanup_actions_for_repos(&repos) {
Some(action) => action,
None => {
return Ok(ResponseJson(ApiResponse::error_with_data(
@@ -1580,34 +1570,19 @@ pub async fn get_first_user_message(
Ok(ResponseJson(ApiResponse::success(message)))
}
-#[derive(Debug, Serialize, Deserialize, TS)]
-#[serde(tag = "type", rename_all = "snake_case")]
-#[ts(tag = "type", rename_all = "snake_case")]
-pub enum DeleteWorkspaceError {
- HasRunningProcesses,
-}
-
pub async fn delete_workspace(
Extension(workspace): Extension,
State(deployment): State,
-) -> Result<
- (
- StatusCode,
- ResponseJson>,
- ),
- ApiError,
-> {
+) -> Result<(StatusCode, ResponseJson>), ApiError> {
let pool = &deployment.db().pool;
// Check for running execution processes
if ExecutionProcess::has_running_non_dev_server_processes_for_workspace(pool, workspace.id)
.await?
{
- return Ok((
- StatusCode::CONFLICT,
- ResponseJson(ApiResponse::error_with_data(
- DeleteWorkspaceError::HasRunningProcesses,
- )),
+ return Err(ApiError::Conflict(
+ "Cannot delete workspace while processes are running. Stop all processes first."
+ .to_string(),
));
}
diff --git a/crates/services/src/services/container.rs b/crates/services/src/services/container.rs
index 5e3c4c6ebb..ae46a06cdc 100644
--- a/crates/services/src/services/container.rs
+++ b/crates/services/src/services/container.rs
@@ -19,7 +19,7 @@ use db::{
CreateExecutionProcessRepoState, ExecutionProcessRepoState,
},
project::{Project, UpdateProject},
- project_repo::{ProjectRepo, ProjectRepoWithName},
+ project_repo::ProjectRepo,
repo::Repo,
session::{CreateSession, Session, SessionError},
task::{Task, TaskStatus},
@@ -372,47 +372,26 @@ pub trait ContainerService {
Repo::update_name(pool, repo.id, &name, &name).await?;
- // Also update dev_script_working_dir and agent_working_dir for single-repo projects
+ // Update agent_working_dir for single-repo projects
let project_repos = ProjectRepo::find_by_repo_id(pool, repo.id).await?;
for pr in project_repos {
let all_repos = ProjectRepo::find_by_project_id(pool, pr.project_id).await?;
if all_repos.len() == 1
&& let Some(project) = Project::find_by_id(pool, pr.project_id).await?
{
- let needs_dev_script_working_dir = project
- .dev_script
- .as_ref()
- .map(|s| !s.is_empty())
- .unwrap_or(false)
- && project
- .dev_script_working_dir
- .as_ref()
- .map(|s| s.is_empty())
- .unwrap_or(true);
-
let needs_default_agent_working_dir = project
.default_agent_working_dir
.as_ref()
.map(|s| s.is_empty())
.unwrap_or(true);
- if needs_dev_script_working_dir || needs_default_agent_working_dir {
+ if needs_default_agent_working_dir {
Project::update(
pool,
pr.project_id,
&UpdateProject {
name: Some(project.name.clone()),
- dev_script: project.dev_script.clone(),
- dev_script_working_dir: if needs_dev_script_working_dir {
- Some(name.clone())
- } else {
- project.dev_script_working_dir.clone()
- },
- default_agent_working_dir: if needs_default_agent_working_dir {
- Some(name.clone())
- } else {
- project.default_agent_working_dir.clone()
- },
+ default_agent_working_dir: Some(name.clone()),
},
)
.await?;
@@ -424,7 +403,7 @@ pub trait ContainerService {
Ok(())
}
- fn cleanup_actions_for_repos(&self, repos: &[ProjectRepoWithName]) -> Option {
+ fn cleanup_actions_for_repos(&self, repos: &[Repo]) -> Option {
let repos_with_cleanup: Vec<_> = repos
.iter()
.filter(|r| r.cleanup_script.is_some())
@@ -441,7 +420,7 @@ pub trait ContainerService {
script: first.cleanup_script.clone().unwrap(),
language: ScriptRequestLanguage::Bash,
context: ScriptContext::CleanupScript,
- working_dir: Some(first.repo_name.clone()),
+ working_dir: Some(first.name.clone()),
}),
None,
);
@@ -452,7 +431,7 @@ pub trait ContainerService {
script: repo.cleanup_script.clone().unwrap(),
language: ScriptRequestLanguage::Bash,
context: ScriptContext::CleanupScript,
- working_dir: Some(repo.repo_name.clone()),
+ working_dir: Some(repo.name.clone()),
}),
None,
));
@@ -461,7 +440,7 @@ pub trait ContainerService {
Some(root_action)
}
- fn setup_actions_for_repos(&self, repos: &[ProjectRepoWithName]) -> Option {
+ fn setup_actions_for_repos(&self, repos: &[Repo]) -> Option {
let repos_with_setup: Vec<_> = repos.iter().filter(|r| r.setup_script.is_some()).collect();
if repos_with_setup.is_empty() {
@@ -475,7 +454,7 @@ pub trait ContainerService {
script: first.setup_script.clone().unwrap(),
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
- working_dir: Some(first.repo_name.clone()),
+ working_dir: Some(first.name.clone()),
}),
None,
);
@@ -486,7 +465,7 @@ pub trait ContainerService {
script: repo.setup_script.clone().unwrap(),
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
- working_dir: Some(repo.repo_name.clone()),
+ working_dir: Some(repo.name.clone()),
}),
None,
));
@@ -495,14 +474,14 @@ pub trait ContainerService {
Some(root_action)
}
- fn setup_action_for_repo(repo: &ProjectRepoWithName) -> Option {
+ fn setup_action_for_repo(repo: &Repo) -> Option {
repo.setup_script.as_ref().map(|script| {
ExecutorAction::new(
ExecutorActionType::ScriptRequest(ScriptRequest {
script: script.clone(),
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
- working_dir: Some(repo.repo_name.clone()),
+ working_dir: Some(repo.name.clone()),
}),
None,
)
@@ -510,7 +489,7 @@ pub trait ContainerService {
}
fn build_sequential_setup_chain(
- repos: &[&ProjectRepoWithName],
+ repos: &[&Repo],
next_action: ExecutorAction,
) -> ExecutorAction {
let mut chained = next_action;
@@ -521,7 +500,7 @@ pub trait ContainerService {
script: script.clone(),
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
- working_dir: Some(repo.repo_name.clone()),
+ working_dir: Some(repo.name.clone()),
}),
Some(Box::new(chained)),
);
@@ -894,14 +873,7 @@ pub trait ContainerService {
.await?
.ok_or(SqlxError::RowNotFound)?;
- // Get parent project
- let project = task
- .parent_project(&self.db().pool)
- .await?
- .ok_or(SqlxError::RowNotFound)?;
-
- let project_repos =
- ProjectRepo::find_by_project_id_with_names(&self.db().pool, project.id).await?;
+ let repos = WorkspaceRepo::find_repos_for_workspace(&self.db().pool, workspace.id).await?;
let workspace = Workspace::find_by_id(&self.db().pool, workspace.id)
.await?
@@ -920,14 +892,11 @@ pub trait ContainerService {
let prompt = task.to_prompt();
- let repos_with_setup: Vec<_> = project_repos
- .iter()
- .filter(|pr| pr.setup_script.is_some())
- .collect();
+ let repos_with_setup: Vec<_> = repos.iter().filter(|r| r.setup_script.is_some()).collect();
- let all_parallel = repos_with_setup.iter().all(|pr| pr.parallel_setup_script);
+ let all_parallel = repos_with_setup.iter().all(|r| r.parallel_setup_script);
- let cleanup_action = self.cleanup_actions_for_repos(&project_repos);
+ let cleanup_action = self.cleanup_actions_for_repos(&repos);
let working_dir = workspace
.agent_working_dir
diff --git a/crates/services/src/services/events.rs b/crates/services/src/services/events.rs
index 7fe6b98feb..f26a9b33c6 100644
--- a/crates/services/src/services/events.rs
+++ b/crates/services/src/services/events.rs
@@ -3,8 +3,8 @@ use std::{str::FromStr, sync::Arc};
use db::{
DBService,
models::{
- execution_process::ExecutionProcess, project::Project, scratch::Scratch, task::Task,
- workspace::Workspace,
+ execution_process::ExecutionProcess, project::Project, scratch::Scratch, session::Session,
+ task::Task, workspace::Workspace,
},
};
use serde_json::json;
@@ -67,7 +67,6 @@ impl EventService {
msg_store: Arc,
session_id: Uuid,
) -> Result<(), SqlxError> {
- use db::models::session::Session;
if let Some(session) = Session::find_by_id(pool, session_id).await?
&& let Some(workspace) = Workspace::find_by_id(pool, session.workspace_id).await?
{
@@ -82,7 +81,6 @@ impl EventService {
msg_store: Arc,
session_id: Uuid,
) -> Result<(), SqlxError> {
- use db::models::session::Session;
if let Some(session) = Session::find_by_id(pool, session_id).await?
&& let Some(workspace_with_status) =
Workspace::find_by_id_with_status(pool, session.workspace_id).await?
diff --git a/crates/services/src/services/project.rs b/crates/services/src/services/project.rs
index 854c08f4d4..b56c8a02d1 100644
--- a/crates/services/src/services/project.rs
+++ b/crates/services/src/services/project.rs
@@ -129,8 +129,6 @@ impl ProjectService {
project.id,
&UpdateProject {
name: None,
- dev_script: None,
- dev_script_working_dir: None,
default_agent_working_dir: Some(repo.name),
},
)
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e2166e250f..f19103c203 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -17,6 +17,7 @@ import {
McpSettings,
OrganizationSettings,
ProjectSettings,
+ ReposSettings,
SettingsLayout,
} from '@/pages/settings/';
import { UserSystemProvider, useUserSystem } from '@/components/ConfigProvider';
@@ -156,6 +157,7 @@ function AppContent() {
} />
} />
} />
+ } />
}
diff --git a/frontend/src/components/NormalizedConversation/NextActionCard.tsx b/frontend/src/components/NormalizedConversation/NextActionCard.tsx
index 611756930b..d23e0a4c3e 100644
--- a/frontend/src/components/NormalizedConversation/NextActionCard.tsx
+++ b/frontend/src/components/NormalizedConversation/NextActionCard.tsx
@@ -17,6 +17,7 @@ import { GitActionsDialog } from '@/components/dialogs/tasks/GitActionsDialog';
import { useOpenInEditor } from '@/hooks/useOpenInEditor';
import { useDiffSummary } from '@/hooks/useDiffSummary';
import { useDevServer } from '@/hooks/useDevServer';
+import { useHasDevServerScript } from '@/hooks/useHasDevServerScript';
import { Button } from '@/components/ui/button';
import { IdeIcon } from '@/components/ide/IdeIcon';
import { useUserSystem } from '@/components/ConfigProvider';
@@ -57,7 +58,7 @@ export function NextActionCard({
}: NextActionCardProps) {
const { t } = useTranslation('tasks');
const { config } = useUserSystem();
- const { project } = useProject();
+ const { projectId } = useProject();
const navigate = useNavigate();
const [copied, setCopied] = useState(false);
@@ -77,11 +78,14 @@ export function NextActionCard({
stop,
isStarting,
isStopping,
- runningDevServer,
- latestDevServerProcess,
+ runningDevServers,
+ devServerProcesses,
} = useDevServer(attemptId);
- const projectHasDevScript = Boolean(project?.dev_script);
+ const hasRunningDevServer = runningDevServers.length > 0;
+
+ const { data: projectHasDevScript = false } =
+ useHasDevServerScript(projectId);
const handleCopy = useCallback(async () => {
if (!containerRef) return;
@@ -103,10 +107,10 @@ export function NextActionCard({
if (sessionId) {
ViewProcessesDialog.show({
sessionId,
- initialProcessId: latestDevServerProcess?.id,
+ initialProcessId: devServerProcesses[0]?.id,
});
}
- }, [sessionId, latestDevServerProcess?.id]);
+ }, [sessionId, devServerProcesses]);
const handleOpenDiffs = useCallback(() => {
navigate({ search: '?view=diffs' });
@@ -302,19 +306,21 @@ export function NextActionCard({
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
- onClick={runningDevServer ? () => stop() : () => start()}
+ onClick={
+ hasRunningDevServer ? () => stop() : () => start()
+ }
disabled={
- (runningDevServer ? isStopping : isStarting) ||
+ (hasRunningDevServer ? isStopping : isStarting) ||
!attemptId ||
!projectHasDevScript
}
aria-label={
- runningDevServer
+ hasRunningDevServer
? t('attempt.pauseDev')
: t('attempt.startDev')
}
>
- {runningDevServer ? (
+ {hasRunningDevServer ? (
) : (
@@ -325,13 +331,13 @@ export function NextActionCard({
{!projectHasDevScript
? t('attempt.devScriptMissingTooltip')
- : runningDevServer
+ : hasRunningDevServer
? t('attempt.pauseDev')
: t('attempt.startDev')}
- {latestDevServerProcess && (
+ {devServerProcesses.length > 0 && (
0;
+
useEffect(() => {
if (
loadingTimeFinished &&
!isReady &&
- latestDevServerProcess &&
- runningDevServer
+ devServerProcesses.length > 0 &&
+ hasRunningDevServer
) {
setShowHelp(true);
setShowLogs(true);
setLoadingTimeFinished(false);
}
- }, [loadingTimeFinished, isReady, latestDevServerProcess, runningDevServer]);
+ }, [
+ loadingTimeFinished,
+ isReady,
+ devServerProcesses.length,
+ hasRunningDevServer,
+ ]);
const isPreviewReady =
(previewState.status === 'ready' && Boolean(previewState.url)) ||
- (customUrl !== null && runningDevServer);
+ (customUrl !== null && hasRunningDevServer);
const isPreviewReadyWithoutError = isPreviewReady && !iframeError;
const mode = iframeError
? 'error'
: isPreviewReadyWithoutError
? 'ready'
- : runningDevServer
+ : hasRunningDevServer
? 'searching'
: 'noServer';
const toggleLogs = () => {
@@ -187,7 +197,7 @@ export function PreviewPanel() {
) : (
)}
diff --git a/frontend/src/components/tasks/TaskDetails/preview/DevServerLogsView.tsx b/frontend/src/components/tasks/TaskDetails/preview/DevServerLogsView.tsx
index 009366fbfd..09ada3fb92 100644
--- a/frontend/src/components/tasks/TaskDetails/preview/DevServerLogsView.tsx
+++ b/frontend/src/components/tasks/TaskDetails/preview/DevServerLogsView.tsx
@@ -1,35 +1,43 @@
+import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Terminal, ChevronDown } from 'lucide-react';
-import ProcessLogsViewer, {
- ProcessLogsViewerContent,
-} from '../ProcessLogsViewer';
+import ProcessLogsViewer from '../ProcessLogsViewer';
+import { getDevServerWorkingDir } from '@/lib/devServerUtils';
+import { cn } from '@/lib/utils';
import { ExecutionProcess } from 'shared/types';
interface DevServerLogsViewProps {
- latestDevServerProcess: ExecutionProcess | undefined;
+ devServerProcesses: ExecutionProcess[];
showLogs: boolean;
onToggle: () => void;
height?: string;
showToggleText?: boolean;
- logs?: Array<{ type: 'STDOUT' | 'STDERR'; content: string }>;
- error?: string | null;
}
export function DevServerLogsView({
- latestDevServerProcess,
+ devServerProcesses,
showLogs,
onToggle,
height = 'h-60',
showToggleText = true,
- logs,
- error,
}: DevServerLogsViewProps) {
const { t } = useTranslation('tasks');
+ const [activeProcessId, setActiveProcessId] = useState(null);
- if (!latestDevServerProcess) {
+ useEffect(() => {
+ if (devServerProcesses.length > 0 && !activeProcessId) {
+ setActiveProcessId(devServerProcesses[0].id);
+ }
+ }, [devServerProcesses, activeProcessId]);
+
+ if (devServerProcesses.length === 0) {
return null;
}
+ const activeProcess =
+ devServerProcesses.find((p) => p.id === activeProcessId) ??
+ devServerProcesses[0];
+
return (
- {logs ? (
-
- ) : (
-
+ {devServerProcesses.length > 1 && (
+
+ {devServerProcesses.map((process) => (
+ setActiveProcessId(process.id)}
+ >
+ {getDevServerWorkingDir(process) ?? 'Dev Server'}
+
+ ))}
+
)}
+
)}
diff --git a/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx b/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx
index 5faa7ebc20..9442ca1325 100644
--- a/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx
+++ b/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx
@@ -1,25 +1,16 @@
-import { useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
import {
Play,
- Edit3,
Square,
SquareTerminal,
- Save,
- X,
+ Settings,
ExternalLink,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
-import { Textarea } from '@/components/ui/textarea';
-import { Alert, AlertDescription } from '@/components/ui/alert';
-import { ExecutionProcess, Project } from 'shared/types';
-import {
- createScriptPlaceholderStrategy,
- ScriptPlaceholderContext,
-} from '@/utils/scriptPlaceholders';
+import { Project } from 'shared/types';
import { useUserSystem } from '@/components/ConfigProvider';
import { useTaskMutations } from '@/hooks/useTaskMutations';
-import { useProjectMutations } from '@/hooks/useProjectMutations';
import { useProjectRepos } from '@/hooks';
import {
COMPANION_INSTALL_TASK_TITLE,
@@ -28,7 +19,7 @@ import {
interface NoServerContentProps {
projectHasDevScript: boolean;
- runningDevServer: ExecutionProcess | undefined;
+ runningDevServer: boolean;
isStartingDevServer: boolean;
startDevServer: () => void;
stopDevServer: () => void;
@@ -44,77 +35,18 @@ export function NoServerContent({
project,
}: NoServerContentProps) {
const { t } = useTranslation('tasks');
- const [devScriptInput, setDevScriptInput] = useState('');
- const [saveError, setSaveError] = useState(null);
- const [isEditingExistingScript, setIsEditingExistingScript] = useState(false);
- const { system, config } = useUserSystem();
+ const navigate = useNavigate();
+ const { config } = useUserSystem();
const { createAndStart } = useTaskMutations(project?.id);
- const { updateProject } = useProjectMutations();
-
const { data: projectRepos = [] } = useProjectRepos(project?.id);
- // Create strategy-based placeholders
- const placeholders = system.environment
- ? new ScriptPlaceholderContext(
- createScriptPlaceholderStrategy(system.environment.os_type)
- ).getPlaceholders()
- : {
- setup: '#!/bin/bash\nnpm install\n# Add any setup commands here...',
- dev: '#!/bin/bash\nnpm run dev\n# Add dev server start command here...',
- cleanup:
- '#!/bin/bash\n# Add cleanup commands here...\n# This runs after coding agent execution',
- };
-
- const handleSaveDevScript = async (startAfterSave?: boolean) => {
- setSaveError(null);
- if (!project) {
- setSaveError(t('preview.devScript.errors.notLoaded'));
- return;
- }
-
- const script = devScriptInput.trim();
- if (!script) {
- setSaveError(t('preview.devScript.errors.empty'));
- return;
- }
-
- updateProject.mutate(
- {
- projectId: project.id,
- data: {
- name: null,
- dev_script: script,
- dev_script_working_dir: project.dev_script_working_dir ?? null,
- default_agent_working_dir: project.default_agent_working_dir ?? null,
- },
- },
- {
- onSuccess: () => {
- setIsEditingExistingScript(false);
- if (startAfterSave) {
- startDevServer();
- }
- },
- onError: (err) => {
- setSaveError((err as Error)?.message || 'Failed to save dev script');
- },
- }
- );
- };
-
- const handleEditExistingScript = () => {
- if (project?.dev_script) {
- setDevScriptInput(project.dev_script);
+ const handleConfigureDevScript = () => {
+ if (projectRepos.length === 1) {
+ navigate(`/settings/repos?repoId=${projectRepos[0].id}`);
+ } else {
+ navigate('/settings/repos');
}
- setIsEditingExistingScript(true);
- setSaveError(null);
- };
-
- const handleCancelEdit = () => {
- setIsEditingExistingScript(false);
- setDevScriptInput('');
- setSaveError(null);
};
const handleInstallCompanion = () => {
@@ -159,114 +91,45 @@ export function NoServerContent({
- {!isEditingExistingScript ? (
-
+
+
{
+ if (runningDevServer) {
+ stopDevServer();
+ } else {
+ startDevServer();
+ }
+ }}
+ disabled={isStartingDevServer || !projectHasDevScript}
+ className="gap-1"
+ >
+ {runningDevServer ? (
+ <>
+
+ {t('preview.toolbar.stopDevServer')}
+ >
+ ) : (
+ <>
+
+ {t('preview.noServer.startButton')}
+ >
+ )}
+
+
+ {!runningDevServer && (
{
- if (runningDevServer) {
- stopDevServer();
- } else {
- startDevServer();
- }
- }}
- disabled={isStartingDevServer || !projectHasDevScript}
+ variant="outline"
+ onClick={handleConfigureDevScript}
className="gap-1"
>
- {runningDevServer ? (
- <>
-
- {t('preview.toolbar.stopDevServer')}
- >
- ) : (
- <>
-
- {t('preview.noServer.startButton')}
- >
- )}
+
+ {t('preview.noServer.configureButton')}
-
- {!runningDevServer && (
-
-
- {t('preview.noServer.editButton')}
-
- )}
-
- ) : (
-
- )}
+ )}
+
diff --git a/frontend/src/components/ui-new/actions/index.ts b/frontend/src/components/ui-new/actions/index.ts
index ef23a5d4a4..6234b3a19d 100644
--- a/frontend/src/components/ui-new/actions/index.ts
+++ b/frontend/src/components/ui-new/actions/index.ts
@@ -1,7 +1,7 @@
import type { Icon } from '@phosphor-icons/react';
import type { NavigateFunction } from 'react-router-dom';
import type { QueryClient } from '@tanstack/react-query';
-import type { EditorType, Workspace } from 'shared/types';
+import type { EditorType, ExecutionProcess, Workspace } from 'shared/types';
import type { DiffViewMode } from '@/stores/useDiffViewStore';
import {
CopyIcon,
@@ -66,7 +66,7 @@ export interface ActionExecutorContext {
activeWorkspaces: SidebarWorkspace[];
currentWorkspaceId: string | null;
containerRef: string | null;
- runningDevServerId: string | null;
+ runningDevServers: ExecutionProcess[];
startDevServer: () => void;
stopDevServer: () => void;
}
@@ -94,7 +94,7 @@ export interface ActionVisibilityContext {
// Dev server state
editorType: EditorType | null;
devServerState: DevServerState;
- runningDevServerId: string | null;
+ runningDevServers: ExecutionProcess[];
// Git panel state
hasGitRepos: boolean;
@@ -586,10 +586,12 @@ export const Actions = {
getLabel: (ctx) =>
ctx.devServerState === 'running' ? 'Stop Dev Server' : 'Start Dev Server',
execute: (ctx) => {
- if (ctx.runningDevServerId) {
+ if (ctx.runningDevServers.length > 0) {
ctx.stopDevServer();
} else {
ctx.startDevServer();
+ // Auto-open preview mode when starting dev server
+ useLayoutStore.getState().setPreviewMode(true);
}
},
},
diff --git a/frontend/src/components/ui-new/actions/useActionVisibility.ts b/frontend/src/components/ui-new/actions/useActionVisibility.ts
index ef1dd1e13f..d839b49aaa 100644
--- a/frontend/src/components/ui-new/actions/useActionVisibility.ts
+++ b/frontend/src/components/ui-new/actions/useActionVisibility.ts
@@ -27,7 +27,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
const diffViewMode = useDiffViewMode();
const expanded = useUiPreferencesStore((s) => s.expanded);
const { config } = useUserSystem();
- const { isStarting, isStopping, runningDevServer } =
+ const { isStarting, isStopping, runningDevServers } =
useDevServer(workspaceId);
return useMemo(() => {
@@ -41,7 +41,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
? 'starting'
: isStopping
? 'stopping'
- : runningDevServer
+ : runningDevServers.length > 0
? 'running'
: 'stopped';
@@ -60,7 +60,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
isAllDiffsExpanded,
editorType: config?.editor?.editor_type ?? null,
devServerState,
- runningDevServerId: runningDevServer?.id ?? null,
+ runningDevServers,
hasGitRepos: repos.length > 0,
hasMultipleRepos: repos.length > 1,
};
@@ -80,7 +80,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
config?.editor?.editor_type,
isStarting,
isStopping,
- runningDevServer,
+ runningDevServers,
]);
}
diff --git a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx
index b012120bdd..da6831b418 100644
--- a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx
+++ b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx
@@ -4,6 +4,8 @@ import { usePreviewDevServer } from '../hooks/usePreviewDevServer';
import { usePreviewUrl } from '../hooks/usePreviewUrl';
import { useLogStream } from '@/hooks/useLogStream';
import { useLayoutStore } from '@/stores/useLayoutStore';
+import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
+import { useNavigate } from 'react-router-dom';
interface PreviewBrowserContainerProps {
attemptId?: string;
@@ -14,12 +16,15 @@ export function PreviewBrowserContainer({
attemptId,
className,
}: PreviewBrowserContainerProps) {
+ const navigate = useNavigate();
const previewRefreshKey = useLayoutStore((s) => s.previewRefreshKey);
+ const { repos } = useWorkspaceContext();
- const { start, isStarting, runningDevServer, latestDevServerProcess } =
+ const { start, isStarting, runningDevServers } =
usePreviewDevServer(attemptId);
- const { logs } = useLogStream(latestDevServerProcess?.id ?? '');
+ const primaryDevServer = runningDevServers[0];
+ const { logs } = useLogStream(primaryDevServer?.id ?? '');
const urlInfo = usePreviewUrl(logs);
const handleStart = useCallback(() => {
@@ -31,13 +36,22 @@ export function PreviewBrowserContainer({
? `${urlInfo.url}${urlInfo.url.includes('?') ? '&' : '?'}_refresh=${previewRefreshKey}`
: undefined;
+ const handleEditDevScript = () => {
+ if (repos.length === 1) {
+ navigate(`/settings/repos?repoId=${repos[0].id}`);
+ } else {
+ navigate('/settings/repos');
+ }
+ };
+
return (
0}
+ repos={repos}
+ handleEditDevScript={handleEditDevScript}
className={className}
/>
);
diff --git a/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx b/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx
index c478cd36d4..8ceb191e78 100644
--- a/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx
+++ b/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx
@@ -1,9 +1,10 @@
-import { useCallback } from 'react';
+import { useCallback, useState, useEffect } from 'react';
import { PreviewControls } from '../views/PreviewControls';
import { usePreviewDevServer } from '../hooks/usePreviewDevServer';
import { usePreviewUrl } from '../hooks/usePreviewUrl';
import { useLogStream } from '@/hooks/useLogStream';
import { useLayoutStore } from '@/stores/useLayoutStore';
+import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
interface PreviewControlsContainerProps {
attemptId?: string;
@@ -16,6 +17,7 @@ export function PreviewControlsContainer({
onViewProcessInPanel,
className,
}: PreviewControlsContainerProps) {
+ const { repos } = useWorkspaceContext();
const setLogsMode = useLayoutStore((s) => s.setLogsMode);
const triggerPreviewRefresh = useLayoutStore((s) => s.triggerPreviewRefresh);
@@ -24,22 +26,43 @@ export function PreviewControlsContainer({
stop,
isStarting,
isStopping,
- runningDevServer,
- latestDevServerProcess,
+ runningDevServers,
+ devServerProcesses,
} = usePreviewDevServer(attemptId);
- const { logs } = useLogStream(latestDevServerProcess?.id ?? '');
- const urlInfo = usePreviewUrl(logs);
+ const [activeProcessId, setActiveProcessId] = useState(null);
- const handleViewFullLogs = useCallback(() => {
- if (latestDevServerProcess?.id && onViewProcessInPanel) {
- // Switch to logs mode and select the dev server process
- onViewProcessInPanel(latestDevServerProcess.id);
- } else {
- // Just switch to logs mode if no process to select
- setLogsMode(true);
+ useEffect(() => {
+ if (devServerProcesses.length > 0 && !activeProcessId) {
+ setActiveProcessId(devServerProcesses[0].id);
}
- }, [latestDevServerProcess?.id, onViewProcessInPanel, setLogsMode]);
+ }, [devServerProcesses, activeProcessId]);
+
+ const activeProcess =
+ devServerProcesses.find((p) => p.id === activeProcessId) ??
+ devServerProcesses[0];
+
+ const { logs, error: logsError } = useLogStream(activeProcess?.id ?? '');
+
+ const primaryDevServer = runningDevServers[0];
+ const { logs: primaryLogs } = useLogStream(primaryDevServer?.id ?? '');
+ const urlInfo = usePreviewUrl(primaryLogs);
+
+ const handleViewFullLogs = useCallback(
+ (processId?: string) => {
+ const targetId = processId ?? activeProcess?.id;
+ if (targetId && onViewProcessInPanel) {
+ onViewProcessInPanel(targetId);
+ } else {
+ setLogsMode(true);
+ }
+ },
+ [activeProcess?.id, onViewProcessInPanel, setLogsMode]
+ );
+
+ const handleTabChange = useCallback((processId: string) => {
+ setActiveProcessId(processId);
+ }, []);
const handleStart = useCallback(() => {
start();
@@ -65,11 +88,24 @@ export function PreviewControlsContainer({
}
}, [urlInfo?.url]);
+ const hasDevScript = repos.some(
+ (repo) => repo.dev_server_script && repo.dev_server_script.trim() !== ''
+ );
+
+ // Don't render if no repos have dev server scripts configured
+ if (!hasDevScript) {
+ return null;
+ }
+
return (
0}
className={className}
/>
);
diff --git a/frontend/src/components/ui-new/hooks/usePreviewDevServer.ts b/frontend/src/components/ui-new/hooks/usePreviewDevServer.ts
index 5b1af384eb..e5e39e7421 100644
--- a/frontend/src/components/ui-new/hooks/usePreviewDevServer.ts
+++ b/frontend/src/components/ui-new/hooks/usePreviewDevServer.ts
@@ -3,7 +3,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi, executionProcessesApi } from '@/lib/api';
import { useAttemptExecution } from '@/hooks/useAttemptExecution';
import { workspaceSummaryKeys } from '@/components/ui-new/hooks/useWorkspaces';
-import type { ExecutionProcess } from 'shared/types';
+import {
+ filterRunningDevServers,
+ filterDevServerProcesses,
+ deduplicateDevServersByWorkingDir,
+} from '@/lib/devServerUtils';
interface UsePreviewDevServerOptions {
onStartSuccess?: () => void;
@@ -19,25 +23,19 @@ export function usePreviewDevServer(
const queryClient = useQueryClient();
const { attemptData } = useAttemptExecution(attemptId);
- // Find running dev server process
- const runningDevServer = useMemo(() => {
- return attemptData.processes.find(
- (process) =>
- process.run_reason === 'devserver' && process.status === 'running'
- );
- }, [attemptData.processes]);
+ const runningDevServers = useMemo(
+ () => filterRunningDevServers(attemptData.processes),
+ [attemptData.processes]
+ );
- // Find latest dev server process (for logs viewing)
- const latestDevServerProcess = useMemo(() => {
- return [...attemptData.processes]
- .filter((process) => process.run_reason === 'devserver')
- .sort(
- (a, b) =>
- new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
- )[0];
- }, [attemptData.processes]);
+ const devServerProcesses = useMemo(
+ () =>
+ deduplicateDevServersByWorkingDir(
+ filterDevServerProcesses(attemptData.processes)
+ ),
+ [attemptData.processes]
+ );
- // Start mutation
const startMutation = useMutation({
mutationKey: ['startDevServer', attemptId],
mutationFn: async () => {
@@ -57,24 +55,25 @@ export function usePreviewDevServer(
},
});
- // Stop mutation
const stopMutation = useMutation({
- mutationKey: ['stopDevServer', runningDevServer?.id],
+ mutationKey: ['stopDevServer', attemptId],
mutationFn: async () => {
- if (!runningDevServer) return;
- await executionProcessesApi.stopExecutionProcess(runningDevServer.id);
+ if (runningDevServers.length === 0) return;
+ await Promise.all(
+ runningDevServers.map((ds) =>
+ executionProcessesApi.stopExecutionProcess(ds.id)
+ )
+ );
},
onSuccess: async () => {
- await Promise.all([
+ await queryClient.invalidateQueries({
+ queryKey: ['executionProcesses', attemptId],
+ });
+ for (const ds of runningDevServers) {
queryClient.invalidateQueries({
- queryKey: ['executionProcesses', attemptId],
- }),
- runningDevServer
- ? queryClient.invalidateQueries({
- queryKey: ['processDetails', runningDevServer.id],
- })
- : Promise.resolve(),
- ]);
+ queryKey: ['processDetails', ds.id],
+ });
+ }
queryClient.invalidateQueries({ queryKey: workspaceSummaryKeys.all });
options?.onStopSuccess?.();
},
@@ -89,7 +88,7 @@ export function usePreviewDevServer(
stop: stopMutation.mutate,
isStarting: startMutation.isPending,
isStopping: stopMutation.isPending,
- runningDevServer,
- latestDevServerProcess,
+ runningDevServers,
+ devServerProcesses,
};
}
diff --git a/frontend/src/components/ui-new/scope/NewDesignScope.tsx b/frontend/src/components/ui-new/scope/NewDesignScope.tsx
index ef3d387c8d..94145dc96e 100644
--- a/frontend/src/components/ui-new/scope/NewDesignScope.tsx
+++ b/frontend/src/components/ui-new/scope/NewDesignScope.tsx
@@ -20,9 +20,12 @@ function ExecutionProcessesProviderWrapper({
}: {
children: ReactNode;
}) {
- const { workspaceId } = useWorkspaceContext();
+ const { workspaceId, selectedSessionId } = useWorkspaceContext();
return (
-
+
{children}
);
diff --git a/frontend/src/components/ui-new/views/PreviewBrowser.tsx b/frontend/src/components/ui-new/views/PreviewBrowser.tsx
index b860e229c8..c2101fdc7e 100644
--- a/frontend/src/components/ui-new/views/PreviewBrowser.tsx
+++ b/frontend/src/components/ui-new/views/PreviewBrowser.tsx
@@ -2,13 +2,15 @@ import { PlayIcon, SpinnerIcon } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { PrimaryButton } from '../primitives/PrimaryButton';
+import { Repo } from 'shared/types';
interface PreviewBrowserProps {
url?: string;
onStart: () => void;
isStarting: boolean;
- hasDevScript: boolean;
isServerRunning: boolean;
+ repos: Repo[];
+ handleEditDevScript: () => void;
className?: string;
}
@@ -16,14 +18,19 @@ export function PreviewBrowser({
url,
onStart,
isStarting,
- hasDevScript,
isServerRunning,
+ repos,
+ handleEditDevScript,
className,
}: PreviewBrowserProps) {
const { t } = useTranslation(['tasks', 'common']);
const isLoading = isStarting || (isServerRunning && !url);
const showIframe = url && !isLoading && isServerRunning;
+ const hasDevScript = repos.some(
+ (repo) => repo.dev_server_script && repo.dev_server_script.trim() !== ''
+ );
+
return (
>
) : (
- <>
- {t('preview.noDevScript')}
-
- {t('preview.noDevScriptHint')}
-
- >
+
+
+
+ You must set up a dev server script to use the preview
+ feature
+
+
+ Vibe Kanban can run dev servers to help you test your
+ changes. You can set up a dev server script in the
+ repository section of the settings page.
+
+
+
+
)}
)}
diff --git a/frontend/src/components/ui-new/views/PreviewControls.tsx b/frontend/src/components/ui-new/views/PreviewControls.tsx
index a3d5716880..f28963b84c 100644
--- a/frontend/src/components/ui-new/views/PreviewControls.tsx
+++ b/frontend/src/components/ui-new/views/PreviewControls.tsx
@@ -10,16 +10,21 @@ import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
import { PrimaryButton } from '../primitives/PrimaryButton';
-import {
- VirtualizedProcessLogs,
- type LogEntry,
-} from '../VirtualizedProcessLogs';
+import { VirtualizedProcessLogs } from '../VirtualizedProcessLogs';
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
+import { getDevServerWorkingDir } from '@/lib/devServerUtils';
+import type { ExecutionProcess, PatchType } from 'shared/types';
+
+type LogEntry = Extract;
interface PreviewControlsProps {
+ devServerProcesses: ExecutionProcess[];
+ activeProcessId: string | null;
logs: LogEntry[];
+ logsError: string | null;
url?: string;
onViewFullLogs: () => void;
+ onTabChange: (processId: string) => void;
onStart: () => void;
onStop: () => void;
onRefresh: () => void;
@@ -27,15 +32,18 @@ interface PreviewControlsProps {
onOpenInNewTab: () => void;
isStarting: boolean;
isStopping: boolean;
- hasDevScript: boolean;
isServerRunning: boolean;
className?: string;
}
export function PreviewControls({
+ devServerProcesses,
+ activeProcessId,
logs,
+ logsError,
url,
onViewFullLogs,
+ onTabChange,
onStart,
onStop,
onRefresh,
@@ -43,7 +51,6 @@ export function PreviewControls({
onOpenInNewTab,
isStarting,
isStopping,
- hasDevScript,
isServerRunning,
className,
}: PreviewControlsProps) {
@@ -62,7 +69,6 @@ export function PreviewControls({
persistKey={PERSIST_KEYS.devServerSection}
contentClassName="flex flex-col flex-1 overflow-hidden"
>
- {/* Controls row: URL bar + Start/Stop button */}
{url && (
@@ -104,19 +110,16 @@ export function PreviewControls({
onClick={onStop}
disabled={isStopping}
/>
- ) : hasDevScript ? (
+ ) : (
- ) : (
-
{t('preview.noDevScript')}
)}
- {/* Logs section */}
@@ -132,14 +135,33 @@ export function PreviewControls({
+ {devServerProcesses.length > 1 && (
+
+ {devServerProcesses.map((process) => (
+ onTabChange(process.id)}
+ >
+ {getDevServerWorkingDir(process) ?? 'Dev Server'}
+
+ ))}
+
+ )}
+
- {isLoading && logs.length === 0 ? (
+ {isLoading && devServerProcesses.length === 0 ? (
- ) : (
-
- )}
+ ) : devServerProcesses.length > 0 ? (
+
+ ) : null}
diff --git a/frontend/src/contexts/ActionsContext.tsx b/frontend/src/contexts/ActionsContext.tsx
index 7088c313c0..2ac1c08dc8 100644
--- a/frontend/src/contexts/ActionsContext.tsx
+++ b/frontend/src/contexts/ActionsContext.tsx
@@ -52,7 +52,7 @@ export function ActionsProvider({ children }: ActionsProviderProps) {
useWorkspaceContext();
// Get dev server state
- const { start, stop, runningDevServer } = useDevServer(workspaceId);
+ const { start, stop, runningDevServers } = useDevServer(workspaceId);
// Build executor context from hooks
const executorContext = useMemo
(
@@ -63,7 +63,7 @@ export function ActionsProvider({ children }: ActionsProviderProps) {
activeWorkspaces,
currentWorkspaceId: workspaceId ?? null,
containerRef: workspace?.container_ref ?? null,
- runningDevServerId: runningDevServer?.id ?? null,
+ runningDevServers,
startDevServer: start,
stopDevServer: stop,
}),
@@ -74,7 +74,7 @@ export function ActionsProvider({ children }: ActionsProviderProps) {
activeWorkspaces,
workspaceId,
workspace?.container_ref,
- runningDevServer?.id,
+ runningDevServers,
start,
stop,
]
diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts
index 8d2a492d2a..0ea3c73686 100644
--- a/frontend/src/hooks/index.ts
+++ b/frontend/src/hooks/index.ts
@@ -18,6 +18,7 @@ export { useTask } from './useTask';
export { useAttempt } from './useAttempt';
export { useRepoBranches } from './useRepoBranches';
export { useProjectRepos } from './useProjectRepos';
+export { useHasDevServerScript } from './useHasDevServerScript';
export { useRepoBranchSelection } from './useRepoBranchSelection';
export type { RepoBranchConfig } from './useRepoBranchSelection';
export { useTaskAttempts } from './useTaskAttempts';
diff --git a/frontend/src/hooks/useCreateModeState.ts b/frontend/src/hooks/useCreateModeState.ts
index e3f65f34fb..b1e356c1d8 100644
--- a/frontend/src/hooks/useCreateModeState.ts
+++ b/frontend/src/hooks/useCreateModeState.ts
@@ -160,14 +160,7 @@ export function useCreateModeState({
for (const draftRepo of scratchData.repos) {
const fullRepo = repoMap.get(draftRepo.repo_id);
if (fullRepo) {
- restoredRepos.push({
- id: fullRepo.id,
- path: fullRepo.path,
- name: fullRepo.name,
- display_name: fullRepo.display_name,
- created_at: fullRepo.created_at,
- updated_at: fullRepo.updated_at,
- });
+ restoredRepos.push(fullRepo);
restoredBranches[draftRepo.repo_id] = draftRepo.target_branch;
}
}
@@ -198,16 +191,7 @@ export function useCreateModeState({
initialRepos.length > 0
) {
hasInitializedRepos.current = true;
- setRepos(
- initialRepos.map((r) => ({
- id: r.id,
- path: r.path,
- name: r.name,
- display_name: r.display_name,
- created_at: r.created_at,
- updated_at: r.updated_at,
- }))
- );
+ setRepos(initialRepos);
setTargetBranches(
initialRepos.reduce(
(acc, repo) => {
diff --git a/frontend/src/hooks/useDevServer.ts b/frontend/src/hooks/useDevServer.ts
index 1ff330311f..152bf20231 100644
--- a/frontend/src/hooks/useDevServer.ts
+++ b/frontend/src/hooks/useDevServer.ts
@@ -3,7 +3,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi, executionProcessesApi } from '@/lib/api';
import { useAttemptExecution } from '@/hooks/useAttemptExecution';
import { workspaceSummaryKeys } from '@/components/ui-new/hooks/useWorkspaces';
-import type { ExecutionProcess } from 'shared/types';
+import {
+ filterRunningDevServers,
+ filterDevServerProcesses,
+ deduplicateDevServersByWorkingDir,
+} from '@/lib/devServerUtils';
interface UseDevServerOptions {
onStartSuccess?: () => void;
@@ -19,25 +23,19 @@ export function useDevServer(
const queryClient = useQueryClient();
const { attemptData } = useAttemptExecution(attemptId);
- // Find running dev server process
- const runningDevServer = useMemo(() => {
- return attemptData.processes.find(
- (process) =>
- process.run_reason === 'devserver' && process.status === 'running'
- );
- }, [attemptData.processes]);
+ const runningDevServers = useMemo(
+ () => filterRunningDevServers(attemptData.processes),
+ [attemptData.processes]
+ );
- // Find latest dev server process (for logs viewing)
- const latestDevServerProcess = useMemo(() => {
- return [...attemptData.processes]
- .filter((process) => process.run_reason === 'devserver')
- .sort(
- (a, b) =>
- new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
- )[0];
- }, [attemptData.processes]);
+ const devServerProcesses = useMemo(
+ () =>
+ deduplicateDevServersByWorkingDir(
+ filterDevServerProcesses(attemptData.processes)
+ ),
+ [attemptData.processes]
+ );
- // Start mutation
const startMutation = useMutation({
mutationKey: ['startDevServer', attemptId],
mutationFn: async () => {
@@ -57,24 +55,25 @@ export function useDevServer(
},
});
- // Stop mutation
const stopMutation = useMutation({
- mutationKey: ['stopDevServer', runningDevServer?.id],
+ mutationKey: ['stopDevServer', attemptId],
mutationFn: async () => {
- if (!runningDevServer) return;
- await executionProcessesApi.stopExecutionProcess(runningDevServer.id);
+ if (runningDevServers.length === 0) return;
+ await Promise.all(
+ runningDevServers.map((ds) =>
+ executionProcessesApi.stopExecutionProcess(ds.id)
+ )
+ );
},
onSuccess: async () => {
- await Promise.all([
+ await queryClient.invalidateQueries({
+ queryKey: ['executionProcesses', attemptId],
+ });
+ for (const ds of runningDevServers) {
queryClient.invalidateQueries({
- queryKey: ['executionProcesses', attemptId],
- }),
- runningDevServer
- ? queryClient.invalidateQueries({
- queryKey: ['processDetails', runningDevServer.id],
- })
- : Promise.resolve(),
- ]);
+ queryKey: ['processDetails', ds.id],
+ });
+ }
queryClient.invalidateQueries({ queryKey: workspaceSummaryKeys.all });
options?.onStopSuccess?.();
},
@@ -89,7 +88,7 @@ export function useDevServer(
stop: stopMutation.mutate,
isStarting: startMutation.isPending,
isStopping: stopMutation.isPending,
- runningDevServer,
- latestDevServerProcess,
+ runningDevServers,
+ devServerProcesses,
};
}
diff --git a/frontend/src/hooks/useHasDevServerScript.ts b/frontend/src/hooks/useHasDevServerScript.ts
new file mode 100644
index 0000000000..0882c429f1
--- /dev/null
+++ b/frontend/src/hooks/useHasDevServerScript.ts
@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+import { projectsApi } from '@/lib/api';
+
+export function useHasDevServerScript(projectId?: string) {
+ return useQuery({
+ queryKey: ['hasDevServerScript', projectId],
+ queryFn: async () => {
+ if (!projectId) return false;
+
+ const repos = await projectsApi.getRepositories(projectId);
+ return repos.some(
+ (repo) => repo.dev_server_script && repo.dev_server_script.trim() !== ''
+ );
+ },
+ enabled: !!projectId,
+ });
+}
diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json
index b41f379153..8b109cfd01 100644
--- a/frontend/src/i18n/locales/en/settings.json
+++ b/frontend/src/i18n/locales/en/settings.json
@@ -6,7 +6,9 @@
"general": "General",
"generalDesc": "Theme, notifications, and preferences",
"projects": "Projects",
- "projectsDesc": "Project scripts and configuration",
+ "projectsDesc": "Project repositories and configuration",
+ "repos": "Repositories",
+ "reposDesc": "Repository scripts and configuration",
"agents": "Agents",
"agentsDesc": "Coding agent configurations",
"mcp": "MCP Servers",
@@ -331,45 +333,72 @@
"helper": "The absolute path to your git repository on disk."
}
},
+ "agentWorkingDir": {
+ "label": "Agent Working Directory",
+ "placeholder": "e.g., my-repo",
+ "helper": "Default directory for new workspaces to run the coding agent from, relative to the workspace root. This value is captured when a workspace is created and won't affect existing workspaces. For single-repo projects, this defaults to the repo name. Leave empty to run from the workspace root."
+ },
+ "save": {
+ "button": "Save Project Settings",
+ "success": "✓ Project settings saved successfully!",
+ "error": "Failed to save project settings",
+ "unsavedChanges": "• You have unsaved changes",
+ "discard": "Discard",
+ "confirmSwitch": "You have unsaved changes. Are you sure you want to switch projects? Your changes will be lost."
+ }
+ },
+ "repos": {
+ "title": "Repository Configuration",
+ "description": "Configure scripts that run when this repository is used in workspaces.",
+ "loading": "Loading repositories...",
+ "loadError": "Failed to load repositories.",
+ "selector": {
+ "label": "Select Repository",
+ "placeholder": "Choose a repository to configure",
+ "helper": "Select a repository to view and edit its configuration.",
+ "noRepos": "No repositories available"
+ },
+ "general": {
+ "title": "General Settings",
+ "description": "Configure basic repository information.",
+ "displayName": {
+ "label": "Display Name",
+ "placeholder": "Enter display name",
+ "helper": "A friendly name for this repository."
+ },
+ "path": {
+ "label": "Repository Path"
+ }
+ },
"scripts": {
"title": "Scripts & Configuration",
- "description": "Configure setup, cleanup, and copy files for this project.",
+ "description": "Configure setup, cleanup, and copy files for this repository. These scripts run whenever the repository is used in any workspace.",
"setup": {
"label": "Setup Script",
"helper": "This script runs from within the worktree after it's created 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",
- "helper": "Starts a development server from task attempts. Scripts execute from the workspace directory (one level above each repo's worktree) unless a working directory is specified below."
- },
- "devWorkingDir": {
- "label": "Dev Server Working Directory",
- "placeholder": "e.g., my-repo",
- "helper": "The directory to run the dev server script from, relative to the workspace root. Leave empty to run from the workspace root."
- },
- "agentWorkingDir": {
- "label": "Agent Working Directory",
- "placeholder": "e.g., my-repo",
- "helper": "Default directory for new workspaces to run the coding agent from, relative to the workspace root. This value is captured when a workspace is created and won't affect existing workspaces. For single-repo projects, this defaults to the repo name. Leave empty to run from the workspace root."
- },
"cleanup": {
"label": "Cleanup Script",
- "helper": "This script runs from within the worktree after coding agent execution, only if changes were made. Use it for quality assurance tasks like running linters, formatters, tests, or other validation steps. If no changes are made, this script is skipped."
+ "helper": "This script runs from within the worktree after coding agent execution, only if changes were made. Use it for quality assurance tasks like running linters, formatters, tests, or other validation steps."
},
"copyFiles": {
"label": "Copy Files",
- "helper": "Comma-separated list of files to copy from the original project directory to the worktree. These files will be copied after the worktree is created but before the setup script runs. Useful for environment-specific files like .env, configuration files, and local settings. Make sure these are gitignored or they could get committed!"
+ "helper": "Comma-separated list of files to copy from the original repository directory to the worktree. Useful for environment files like .env. Make sure these are gitignored!"
+ },
+ "devServer": {
+ "label": "Dev Server Script",
+ "helper": "Starts a development server for this repository. Scripts execute from within the repository's worktree directory."
}
},
"save": {
- "button": "Save Project Settings",
- "success": "✓ Project settings saved successfully!",
- "error": "Failed to save project settings",
- "unsavedChanges": "• You have unsaved changes",
+ "button": "Save Repository Settings",
+ "success": "Repository settings saved successfully!",
+ "error": "Failed to save repository settings",
+ "unsavedChanges": "You have unsaved changes",
"discard": "Discard",
- "confirmSwitch": "You have unsaved changes. Are you sure you want to switch projects? Your changes will be lost."
+ "confirmSwitch": "You have unsaved changes. Are you sure you want to switch repositories? Your changes will be lost."
}
}
},
diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json
index b3c2c3a0f9..f35b550b40 100644
--- a/frontend/src/i18n/locales/en/tasks.json
+++ b/frontend/src/i18n/locales/en/tasks.json
@@ -88,7 +88,7 @@
"companionPrompt": "For click-to-edit functionality, add the browser companion to your project.",
"companionLink": "View installation guide",
"startButton": "Start Dev Server",
- "editButton": "Edit Dev Script",
+ "configureButton": "Configure",
"stopAndEditButton": "Stop Dev Server & Resolve Issues"
},
"devScript": {
diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json
index 37983c0b17..33295a501b 100644
--- a/frontend/src/i18n/locales/es/settings.json
+++ b/frontend/src/i18n/locales/es/settings.json
@@ -6,7 +6,9 @@
"general": "General",
"generalDesc": "Tema, notificaciones y preferencias",
"projects": "Proyectos",
- "projectsDesc": "Scripts y configuración de proyectos",
+ "projectsDesc": "Repositorios de proyectos y configuración",
+ "repos": "Repositorios",
+ "reposDesc": "Scripts y configuración de repositorios",
"agents": "Agentes",
"agentsDesc": "Configuraciones de agentes",
"mcp": "Servidores MCP",
@@ -331,45 +333,72 @@
"helper": "La ruta absoluta a tu repositorio git en disco."
}
},
+ "agentWorkingDir": {
+ "label": "Directorio de Trabajo del Agente",
+ "placeholder": "ej., mi-repo",
+ "helper": "Directorio predeterminado para nuevos workspaces donde ejecutar el agente de codificación, relativo a la raíz del workspace. Este valor se captura cuando se crea un workspace y no afectará a los workspaces existentes. Para proyectos de un solo repositorio, esto se establece por defecto al nombre del repositorio. Déjalo vacío para ejecutar desde la raíz del workspace."
+ },
+ "save": {
+ "button": "Guardar Configuración del Proyecto",
+ "success": "✓ ¡Configuración del proyecto guardada exitosamente!",
+ "error": "Error al guardar la configuración del proyecto",
+ "unsavedChanges": "• Tienes cambios sin guardar",
+ "discard": "Descartar",
+ "confirmSwitch": "Tienes cambios sin guardar. ¿Estás seguro de que quieres cambiar de proyecto? Tus cambios se perderán."
+ }
+ },
+ "repos": {
+ "title": "Configuración del Repositorio",
+ "description": "Configura los scripts que se ejecutan cuando este repositorio se usa en workspaces.",
+ "loading": "Cargando repositorios...",
+ "loadError": "Error al cargar repositorios.",
+ "selector": {
+ "label": "Seleccionar Repositorio",
+ "placeholder": "Elige un repositorio para configurar",
+ "helper": "Selecciona un repositorio para ver y editar su configuración.",
+ "noRepos": "No hay repositorios disponibles"
+ },
+ "general": {
+ "title": "Configuración General",
+ "description": "Configura la información básica del repositorio.",
+ "displayName": {
+ "label": "Nombre para Mostrar",
+ "placeholder": "Ingresa el nombre para mostrar",
+ "helper": "Un nombre amigable para este repositorio."
+ },
+ "path": {
+ "label": "Ruta del Repositorio"
+ }
+ },
"scripts": {
"title": "Scripts y Configuración",
- "description": "Configura los scripts de instalación, limpieza y archivos a copiar para este proyecto.",
+ "description": "Configura los scripts de instalación, limpieza y archivos a copiar para este repositorio. Estos scripts se ejecutan cada vez que el repositorio se usa en cualquier workspace.",
"setup": {
"label": "Script de Instalación",
"helper": "Este script se ejecuta desde dentro del worktree después de crearse 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",
- "helper": "Inicia un servidor de desarrollo desde los intentos de tarea. Los scripts se ejecutan desde el directorio del workspace (un nivel arriba del worktree de cada repositorio) a menos que se especifique un directorio de trabajo abajo."
- },
- "devWorkingDir": {
- "label": "Directorio de Trabajo del Servidor de Desarrollo",
- "placeholder": "ej., mi-repo",
- "helper": "El directorio desde el cual ejecutar el script del servidor de desarrollo, relativo a la raíz del workspace. Déjalo vacío para ejecutar desde la raíz del workspace."
- },
- "agentWorkingDir": {
- "label": "Directorio de Trabajo del Agente",
- "placeholder": "ej., mi-repo",
- "helper": "Directorio predeterminado para nuevos workspaces donde ejecutar el agente de codificación, relativo a la raíz del workspace. Este valor se captura cuando se crea un workspace y no afectará a los workspaces existentes. Para proyectos de un solo repositorio, esto se establece por defecto al nombre del repositorio. Déjalo vacío para ejecutar desde la raíz del workspace."
- },
"cleanup": {
"label": "Script de Limpieza",
- "helper": "Este script se ejecuta desde dentro del worktree después de la ejecución del agente de codificación, solo si se realizaron cambios. Úsalo para tareas de garantía de calidad como ejecutar linters, formateadores, pruebas u otros pasos de validación. Si no se realizan cambios, se omite este script."
+ "helper": "Este script se ejecuta desde dentro del worktree después de la ejecución del agente de codificación, solo si se realizaron cambios. Úsalo para tareas de garantía de calidad como ejecutar linters, formateadores, pruebas u otros pasos de validación."
},
"copyFiles": {
"label": "Copiar Archivos",
- "helper": "Lista separada por comas de archivos para copiar del directorio del proyecto original al worktree. Estos archivos se copiarán después de que se cree el worktree pero antes de que se ejecute el script de configuración. Útil para archivos específicos del entorno como .env, archivos de configuración y ajustes locales. ¡Asegúrate de que estén en gitignore o podrían ser confirmados!"
+ "helper": "Lista separada por comas de archivos para copiar del directorio del repositorio original al worktree. Útil para archivos de entorno como .env. ¡Asegúrate de que estén en gitignore!"
+ },
+ "devServer": {
+ "label": "Script del Servidor de Desarrollo",
+ "helper": "Inicia un servidor de desarrollo para este repositorio. Los scripts se ejecutan desde el directorio worktree del repositorio."
}
},
"save": {
- "button": "Guardar Configuración del Proyecto",
- "success": "✓ ¡Configuración del proyecto guardada exitosamente!",
- "error": "Error al guardar la configuración del proyecto",
- "unsavedChanges": "• Tienes cambios sin guardar",
+ "button": "Guardar Configuración del Repositorio",
+ "success": "¡Configuración del repositorio guardada exitosamente!",
+ "error": "Error al guardar la configuración del repositorio",
+ "unsavedChanges": "Tienes cambios sin guardar",
"discard": "Descartar",
- "confirmSwitch": "Tienes cambios sin guardar. ¿Estás seguro de que quieres cambiar de proyecto? Tus cambios se perderán."
+ "confirmSwitch": "Tienes cambios sin guardar. ¿Estás seguro de que quieres cambiar de repositorio? Tus cambios se perderán."
}
}
},
diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json
index 762941fb28..30d62623ca 100644
--- a/frontend/src/i18n/locales/es/tasks.json
+++ b/frontend/src/i18n/locales/es/tasks.json
@@ -354,7 +354,7 @@
"noServer": {
"companionLink": "Ver guía de instalación",
"companionPrompt": "Para la funcionalidad de clic y editar, agrega el complemento del navegador a tu proyecto.",
- "editButton": "Editar Script de Desarrollo",
+ "configureButton": "Configurar",
"setupPrompt": "Para usar la vista previa en vivo y la función de hacer clic y editar, por favor agrega un script de servidor de desarrollo a este proyecto.",
"startButton": "Iniciar Servidor de Desarrollo",
"startPrompt": "Por favor inicia un servidor de desarrollo para ver la vista previa",
diff --git a/frontend/src/i18n/locales/ja/settings.json b/frontend/src/i18n/locales/ja/settings.json
index aa5bcc854c..4ee1b39971 100644
--- a/frontend/src/i18n/locales/ja/settings.json
+++ b/frontend/src/i18n/locales/ja/settings.json
@@ -6,7 +6,9 @@
"general": "一般",
"generalDesc": "テーマ、通知、および設定",
"projects": "プロジェクト",
- "projectsDesc": "プロジェクトスクリプトと設定",
+ "projectsDesc": "プロジェクトリポジトリと設定",
+ "repos": "リポジトリ",
+ "reposDesc": "リポジトリスクリプトと設定",
"agents": "エージェント",
"agentsDesc": "コーディングエージェントの設定",
"mcp": "MCPサーバー",
@@ -331,45 +333,72 @@
"helper": "ディスク上のgitリポジトリへの絶対パス。"
}
},
+ "agentWorkingDir": {
+ "label": "エージェント作業ディレクトリ",
+ "placeholder": "例:my-repo",
+ "helper": "新しいワークスペースでコーディングエージェントを実行するデフォルトディレクトリ。ワークスペースルートからの相対パス。この値はワークスペース作成時に保存され、既存のワークスペースには影響しません。単一リポジトリプロジェクトの場合、リポジトリ名がデフォルトになります。空欄にするとワークスペースルートから実行します。"
+ },
+ "save": {
+ "button": "プロジェクト設定を保存",
+ "success": "✓ プロジェクト設定が正常に保存されました!",
+ "error": "プロジェクト設定の保存に失敗しました",
+ "unsavedChanges": "• 未保存の変更があります",
+ "discard": "破棄",
+ "confirmSwitch": "未保存の変更があります。本当にプロジェクトを切り替えますか?変更は失われます。"
+ }
+ },
+ "repos": {
+ "title": "リポジトリ設定",
+ "description": "このリポジトリがワークスペースで使用されるときに実行されるスクリプトを設定します。",
+ "loading": "リポジトリを読み込み中...",
+ "loadError": "リポジトリの読み込みに失敗しました。",
+ "selector": {
+ "label": "リポジトリを選択",
+ "placeholder": "設定するリポジトリを選択",
+ "helper": "リポジトリを選択して、その設定を表示および編集します。",
+ "noRepos": "利用可能なリポジトリがありません"
+ },
+ "general": {
+ "title": "一般設定",
+ "description": "リポジトリの基本情報を設定します。",
+ "displayName": {
+ "label": "表示名",
+ "placeholder": "表示名を入力",
+ "helper": "このリポジトリのわかりやすい名前。"
+ },
+ "path": {
+ "label": "リポジトリパス"
+ }
+ },
"scripts": {
"title": "スクリプトと設定",
- "description": "このプロジェクトのセットアップ、クリーンアップスクリプト、およびコピーするファイルを設定します。",
+ "description": "このリポジトリのセットアップ、クリーンアップスクリプト、およびコピーするファイルを設定します。これらのスクリプトは、リポジトリがどのワークスペースでも使用されるたびに実行されます。",
"setup": {
"label": "セットアップスクリプト",
"helper": "このスクリプトはワークツリー内から、作成後かつコーディングエージェントの開始前に実行されます。依存関係のインストールや環境の準備などのセットアップタスクに使用してください。",
"parallelLabel": "セットアップスクリプトをコーディングエージェントと並行して実行",
"parallelHelper": "有効にすると、セットアップスクリプトはセットアップの完了を待たずに、コーディングエージェントと同時に実行されます。"
},
- "dev": {
- "label": "開発サーバースクリプト",
- "helper": "タスク試行から開発サーバーを起動します。スクリプトは、下記で作業ディレクトリが指定されていない限り、ワークスペースディレクトリ(各リポジトリのワークツリーの1つ上のレベル)から実行されます。"
- },
- "devWorkingDir": {
- "label": "開発サーバー作業ディレクトリ",
- "placeholder": "例:my-repo",
- "helper": "開発サーバースクリプトを実行するディレクトリ。ワークスペースルートからの相対パス。空欄にするとワークスペースルートから実行します。"
- },
- "agentWorkingDir": {
- "label": "エージェント作業ディレクトリ",
- "placeholder": "例:my-repo",
- "helper": "新しいワークスペースでコーディングエージェントを実行するデフォルトディレクトリ。ワークスペースルートからの相対パス。この値はワークスペース作成時に保存され、既存のワークスペースには影響しません。単一リポジトリプロジェクトの場合、リポジトリ名がデフォルトになります。空欄にするとワークスペースルートから実行します。"
- },
"cleanup": {
"label": "クリーンアップスクリプト",
- "helper": "このスクリプトはワークツリー内から、コーディングエージェントの実行後に実行されます(変更が行われた場合のみ)。リンター、フォーマッター、テスト、またはその他の検証ステップの実行など、品質保証タスクに使用してください。変更がない場合、このスクリプトはスキップされます。"
+ "helper": "このスクリプトはワークツリー内から、コーディングエージェントの実行後に実行されます(変更が行われた場合のみ)。リンター、フォーマッター、テスト、またはその他の検証ステップの実行など、品質保証タスクに使用してください。"
},
"copyFiles": {
"label": "ファイルをコピー",
- "helper": "元のプロジェクトディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。これらのファイルは、ワークツリーが作成された後、セットアップスクリプトが実行される前にコピーされます。.env、設定ファイル、ローカル設定などの環境固有のファイルに役立ちます。gitignoreされていることを確認してください。そうしないとコミットされる可能性があります!"
+ "helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください!"
+ },
+ "devServer": {
+ "label": "開発サーバースクリプト",
+ "helper": "このリポジトリの開発サーバーを起動します。スクリプトはリポジトリのワークツリーディレクトリから実行されます。"
}
},
"save": {
- "button": "プロジェクト設定を保存",
- "success": "✓ プロジェクト設定が正常に保存されました!",
- "error": "プロジェクト設定の保存に失敗しました",
- "unsavedChanges": "• 未保存の変更があります",
+ "button": "リポジトリ設定を保存",
+ "success": "リポジトリ設定が正常に保存されました!",
+ "error": "リポジトリ設定の保存に失敗しました",
+ "unsavedChanges": "未保存の変更があります",
"discard": "破棄",
- "confirmSwitch": "未保存の変更があります。本当にプロジェクトを切り替えますか?変更は失われます。"
+ "confirmSwitch": "未保存の変更があります。本当にリポジトリを切り替えますか?変更は失われます。"
}
}
},
diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json
index 159a145b3a..8b0eb40151 100644
--- a/frontend/src/i18n/locales/ja/tasks.json
+++ b/frontend/src/i18n/locales/ja/tasks.json
@@ -354,7 +354,7 @@
"noServer": {
"companionLink": "インストールガイドを表示",
"companionPrompt": "クリック編集機能のために、プロジェクトにブラウザコンパニオンを追加してください。",
- "editButton": "開発スクリプトを編集",
+ "configureButton": "設定",
"setupPrompt": "ライブプレビューとクリックして編集機能を使用するには、このプロジェクトに開発サーバースクリプトを追加してください。",
"startButton": "開発サーバーを開始",
"startPrompt": "プレビューを表示するには開発サーバーを起動してください",
diff --git a/frontend/src/i18n/locales/ko/settings.json b/frontend/src/i18n/locales/ko/settings.json
index 92acc2ff9c..3e9ce49d70 100644
--- a/frontend/src/i18n/locales/ko/settings.json
+++ b/frontend/src/i18n/locales/ko/settings.json
@@ -6,7 +6,9 @@
"general": "일반",
"generalDesc": "테마, 알림 및 환경설정",
"projects": "프로젝트",
- "projectsDesc": "프로젝트 스크립트 및 구성",
+ "projectsDesc": "프로젝트 저장소 및 구성",
+ "repos": "저장소",
+ "reposDesc": "저장소 스크립트 및 구성",
"agents": "에이전트",
"agentsDesc": "코딩 에이전트 구성",
"mcp": "MCP 서버",
@@ -331,45 +333,72 @@
"helper": "디스크에 있는 git 저장소의 절대 경로입니다."
}
},
+ "agentWorkingDir": {
+ "label": "에이전트 작업 디렉토리",
+ "placeholder": "예: my-repo",
+ "helper": "새 워크스페이스에서 코딩 에이전트를 실행할 기본 디렉토리로, 워크스페이스 루트 기준 상대 경로입니다. 이 값은 워크스페이스 생성 시 저장되며 기존 워크스페이스에는 영향을 주지 않습니다. 단일 저장소 프로젝트의 경우 저장소 이름이 기본값입니다. 비워두면 워크스페이스 루트에서 실행됩니다."
+ },
+ "save": {
+ "button": "프로젝트 설정 저장",
+ "success": "✓ 프로젝트 설정이 성공적으로 저장되었습니다!",
+ "error": "프로젝트 설정을 저장하지 못했습니다",
+ "unsavedChanges": "• 저장되지 않은 변경사항이 있습니다",
+ "discard": "취소",
+ "confirmSwitch": "저장되지 않은 변경사항이 있습니다. 정말 프로젝트를 전환하시겠습니까? 변경사항이 손실됩니다."
+ }
+ },
+ "repos": {
+ "title": "저장소 구성",
+ "description": "이 저장소가 워크스페이스에서 사용될 때 실행되는 스크립트를 구성합니다.",
+ "loading": "저장소 로딩 중...",
+ "loadError": "저장소를 불러오지 못했습니다.",
+ "selector": {
+ "label": "저장소 선택",
+ "placeholder": "구성할 저장소를 선택하세요",
+ "helper": "저장소를 선택하여 구성을 보고 편집하세요.",
+ "noRepos": "사용 가능한 저장소가 없습니다"
+ },
+ "general": {
+ "title": "일반 설정",
+ "description": "저장소 기본 정보를 구성합니다.",
+ "displayName": {
+ "label": "표시 이름",
+ "placeholder": "표시 이름 입력",
+ "helper": "이 저장소의 친숙한 이름입니다."
+ },
+ "path": {
+ "label": "저장소 경로"
+ }
+ },
"scripts": {
"title": "스크립트 및 구성",
- "description": "이 프로젝트의 설정, 정리 스크립트 및 복사할 파일을 구성하세요.",
+ "description": "이 저장소의 설정, 정리 스크립트 및 복사할 파일을 구성합니다. 이러한 스크립트는 저장소가 모든 워크스페이스에서 사용될 때마다 실행됩니다.",
"setup": {
"label": "설정 스크립트",
"helper": "이 스크립트는 워크트리 내부에서 생성 후 코딩 에이전트가 시작되기 전에 실행됩니다. 종속성 설치 또는 환경 준비와 같은 설정 작업에 사용하세요.",
"parallelLabel": "설정 스크립트를 코딩 에이전트와 병렬로 실행",
"parallelHelper": "활성화되면 설정 스크립트가 설정 완료를 기다리지 않고 코딩 에이전트와 동시에 실행됩니다."
},
- "dev": {
- "label": "개발 서버 스크립트",
- "helper": "작업 시도에서 개발 서버를 시작합니다. 스크립트는 아래에서 작업 디렉토리를 지정하지 않는 한 워크스페이스 디렉토리(각 저장소의 워크트리보다 한 단계 위)에서 실행됩니다."
- },
- "devWorkingDir": {
- "label": "개발 서버 작업 디렉토리",
- "placeholder": "예: my-repo",
- "helper": "개발 서버 스크립트를 실행할 디렉토리로, 워크스페이스 루트 기준 상대 경로입니다. 비워두면 워크스페이스 루트에서 실행됩니다."
- },
- "agentWorkingDir": {
- "label": "에이전트 작업 디렉토리",
- "placeholder": "예: my-repo",
- "helper": "새 워크스페이스에서 코딩 에이전트를 실행할 기본 디렉토리로, 워크스페이스 루트 기준 상대 경로입니다. 이 값은 워크스페이스 생성 시 저장되며 기존 워크스페이스에는 영향을 주지 않습니다. 단일 저장소 프로젝트의 경우 저장소 이름이 기본값입니다. 비워두면 워크스페이스 루트에서 실행됩니다."
- },
"cleanup": {
"label": "정리 스크립트",
- "helper": "이 스크립트는 워크트리 내부에서 코딩 에이전트 실행 후에 실행됩니다(변경 사항이 있는 경우에만). 린터, 포맷터, 테스트 또는 기타 검증 단계 실행과 같은 품질 보증 작업에 사용하세요. 변경 사항이 없으면 이 스크립트를 건너뜁니다."
+ "helper": "이 스크립트는 워크트리 내부에서 코딩 에이전트 실행 후에 실행됩니다(변경 사항이 있는 경우에만). 린터, 포맷터, 테스트 또는 기타 검증 단계 실행과 같은 품질 보증 작업에 사용하세요."
},
"copyFiles": {
"label": "파일 복사",
- "helper": "원래 프로젝트 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. 이러한 파일은 워크트리가 생성된 후 설정 스크립트가 실행되기 전에 복사됩니다. .env, 구성 파일 및 로컬 설정과 같은 환경별 파일에 유용합니다. gitignore되었는지 확인하세요. 그렇지 않으면 커밋될 수 있습니다!"
+ "helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!"
+ },
+ "devServer": {
+ "label": "개발 서버 스크립트",
+ "helper": "이 저장소의 개발 서버를 시작합니다. 스크립트는 저장소의 워크트리 디렉토리에서 실행됩니다."
}
},
"save": {
- "button": "프로젝트 설정 저장",
- "success": "✓ 프로젝트 설정이 성공적으로 저장되었습니다!",
- "error": "프로젝트 설정을 저장하지 못했습니다",
- "unsavedChanges": "• 저장되지 않은 변경사항이 있습니다",
+ "button": "저장소 설정 저장",
+ "success": "저장소 설정이 성공적으로 저장되었습니다!",
+ "error": "저장소 설정을 저장하지 못했습니다",
+ "unsavedChanges": "저장되지 않은 변경사항이 있습니다",
"discard": "취소",
- "confirmSwitch": "저장되지 않은 변경사항이 있습니다. 정말 프로젝트를 전환하시겠습니까? 변경사항이 손실됩니다."
+ "confirmSwitch": "저장되지 않은 변경사항이 있습니다. 정말 저장소를 전환하시겠습니까? 변경사항이 손실됩니다."
}
}
},
diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json
index 7f4b4ad0bd..bb81da1b7c 100644
--- a/frontend/src/i18n/locales/ko/tasks.json
+++ b/frontend/src/i18n/locales/ko/tasks.json
@@ -346,7 +346,7 @@
"noServer": {
"companionLink": "설치 가이드 보기",
"companionPrompt": "클릭하여 편집 기능을 사용하려면 프로젝트에 브라우저 컴패니언을 추가하세요.",
- "editButton": "개발 스크립트 편집",
+ "configureButton": "구성",
"setupPrompt": "라이브 미리보기 및 클릭하여 편집을 사용하려면 이 프로젝트에 개발 서버 스크립트를 추가하세요.",
"startButton": "개발 서버 시작",
"startPrompt": "미리보기를 보려면 개발 서버를 시작하세요",
diff --git a/frontend/src/i18n/locales/zh-Hans/settings.json b/frontend/src/i18n/locales/zh-Hans/settings.json
index 5f9979ad61..92cb7122be 100644
--- a/frontend/src/i18n/locales/zh-Hans/settings.json
+++ b/frontend/src/i18n/locales/zh-Hans/settings.json
@@ -6,7 +6,9 @@
"general": "常规",
"generalDesc": "主题、通知和偏好设置",
"projects": "项目",
- "projectsDesc": "项目脚本和配置",
+ "projectsDesc": "项目仓库和配置",
+ "repos": "仓库",
+ "reposDesc": "仓库脚本和配置",
"agents": "代理",
"agentsDesc": "编码代理配置",
"mcp": "MCP 服务器",
@@ -331,45 +333,72 @@
"helper": "磁盘上 git 仓库的绝对路径。"
}
},
+ "agentWorkingDir": {
+ "label": "代理工作目录",
+ "placeholder": "例如:my-repo",
+ "helper": "新工作区运行编码代理的默认目录,相对于工作区根目录。此值在创建工作区时保存,不会影响现有工作区。对于单仓库项目,默认为仓库名称。留空则从工作区根目录运行。"
+ },
+ "save": {
+ "button": "保存项目设置",
+ "success": "✓ 项目设置保存成功!",
+ "error": "保存项目设置失败",
+ "unsavedChanges": "• 您有未保存的更改",
+ "discard": "放弃",
+ "confirmSwitch": "您有未保存的更改。您确定要切换项目吗?您的更改将丢失。"
+ }
+ },
+ "repos": {
+ "title": "仓库配置",
+ "description": "配置此仓库在工作区中使用时运行的脚本。",
+ "loading": "加载仓库中...",
+ "loadError": "加载仓库失败。",
+ "selector": {
+ "label": "选择仓库",
+ "placeholder": "选择要配置的仓库",
+ "helper": "选择仓库以查看和编辑其配置。",
+ "noRepos": "没有可用的仓库"
+ },
+ "general": {
+ "title": "常规设置",
+ "description": "配置仓库基本信息。",
+ "displayName": {
+ "label": "显示名称",
+ "placeholder": "输入显示名称",
+ "helper": "此仓库的友好名称。"
+ },
+ "path": {
+ "label": "仓库路径"
+ }
+ },
"scripts": {
"title": "脚本和配置",
- "description": "为此项目配置设置脚本、清理脚本和要复制的文件。",
+ "description": "配置此仓库的设置脚本、清理脚本和要复制的文件。这些脚本在仓库用于任何工作区时都会运行。",
"setup": {
"label": "设置脚本",
"helper": "此脚本从工作树内部运行,在创建后、编码代理启动前执行。用于设置任务,如安装依赖项或准备环境。",
"parallelLabel": "与编码代理并行运行设置脚本",
"parallelHelper": "启用后,设置脚本将与编码代理同时运行,而不是等待设置完成后再启动。"
},
- "dev": {
- "label": "开发服务器脚本",
- "helper": "从任务尝试中启动开发服务器。脚本从工作区目录(每个仓库工作树的上一级)执行,除非在下方指定了工作目录。"
- },
- "devWorkingDir": {
- "label": "开发服务器工作目录",
- "placeholder": "例如:my-repo",
- "helper": "运行开发服务器脚本的目录,相对于工作区根目录。留空则从工作区根目录运行。"
- },
- "agentWorkingDir": {
- "label": "代理工作目录",
- "placeholder": "例如:my-repo",
- "helper": "新工作区运行编码代理的默认目录,相对于工作区根目录。此值在创建工作区时保存,不会影响现有工作区。对于单仓库项目,默认为仓库名称。留空则从工作区根目录运行。"
- },
"cleanup": {
"label": "清理脚本",
- "helper": "此脚本从工作树内部运行,在编码代理执行后执行(仅在进行了更改时)。用于质量保证任务,如运行 linter、格式化程序、测试或其他验证步骤。如果没有进行更改,则跳过此脚本。"
+ "helper": "此脚本从工作树内部运行,在编码代理执行后执行(仅在进行了更改时)。用于质量保证任务,如运行 linter、格式化程序、测试或其他验证步骤。"
},
"copyFiles": {
"label": "复制文件",
- "helper": "要从原始项目目录复制到工作树的文件的逗号分隔列表。这些文件将在创建工作树后但在运行设置脚本之前复制。对环境特定文件(如 .env、配置文件和本地设置)很有用。确保这些文件被 gitignore,否则它们可能会被提交!"
+ "helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore!"
+ },
+ "devServer": {
+ "label": "开发服务器脚本",
+ "helper": "为此仓库启动开发服务器。脚本从仓库的工作树目录执行。"
}
},
"save": {
- "button": "保存项目设置",
- "success": "✓ 项目设置保存成功!",
- "error": "保存项目设置失败",
- "unsavedChanges": "• 您有未保存的更改",
+ "button": "保存仓库设置",
+ "success": "仓库设置保存成功!",
+ "error": "保存仓库设置失败",
+ "unsavedChanges": "您有未保存的更改",
"discard": "放弃",
- "confirmSwitch": "您有未保存的更改。您确定要切换项目吗?您的更改将丢失。"
+ "confirmSwitch": "您有未保存的更改。您确定要切换仓库吗?您的更改将丢失。"
}
}
},
diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json
index 0569550720..9d47627da5 100644
--- a/frontend/src/i18n/locales/zh-Hans/tasks.json
+++ b/frontend/src/i18n/locales/zh-Hans/tasks.json
@@ -101,7 +101,7 @@
"companionPrompt": "要使用点击编辑功能,请将浏览器伴侣添加到您的项目。",
"companionLink": "查看安装指南",
"startButton": "启动开发服务器",
- "editButton": "编辑开发脚本",
+ "configureButton": "配置",
"stopAndEditButton": "停止开发服务器并解决问题"
},
"devScript": {
diff --git a/frontend/src/i18n/locales/zh-Hant/settings.json b/frontend/src/i18n/locales/zh-Hant/settings.json
index 1e98644c74..0b48784126 100644
--- a/frontend/src/i18n/locales/zh-Hant/settings.json
+++ b/frontend/src/i18n/locales/zh-Hant/settings.json
@@ -6,7 +6,9 @@
"general": "一般",
"generalDesc": "主題、通知與偏好設定",
"projects": "專案",
- "projectsDesc": "專案腳本與設定",
+ "projectsDesc": "專案儲存庫與設定",
+ "repos": "儲存庫",
+ "reposDesc": "儲存庫腳本與設定",
"agents": "代理",
"agentsDesc": "編碼代理設定",
"mcp": "MCP 伺服器",
@@ -331,45 +333,72 @@
"helper": "磁碟上的 Git 儲存庫絕對路徑。"
}
},
+ "agentWorkingDir": {
+ "label": "代理工作目錄",
+ "placeholder": "例如:my-repo",
+ "helper": "新工作區執行編碼代理的預設目錄,相對於工作區根目錄。此值在建立工作區時保存,不會影響現有工作區。單一儲存庫專案預設為儲存庫名稱。留空則從工作區根目錄執行。"
+ },
+ "save": {
+ "button": "儲存專案設定",
+ "success": "✓ 專案設定儲存成功!",
+ "error": "儲存專案設定失敗",
+ "unsavedChanges": "• 您有未儲存的變更",
+ "discard": "放棄",
+ "confirmSwitch": "您有未儲存的變更。確定要切換專案嗎?您的變更將會遺失。"
+ }
+ },
+ "repos": {
+ "title": "儲存庫設定",
+ "description": "設定此儲存庫在工作區中使用時執行的腳本。",
+ "loading": "載入儲存庫中...",
+ "loadError": "載入儲存庫失敗。",
+ "selector": {
+ "label": "選擇儲存庫",
+ "placeholder": "選擇要設定的儲存庫",
+ "helper": "選擇儲存庫以查看與編輯其設定。",
+ "noRepos": "沒有可用的儲存庫"
+ },
+ "general": {
+ "title": "一般設定",
+ "description": "設定基本儲存庫資訊。",
+ "displayName": {
+ "label": "顯示名稱",
+ "placeholder": "輸入顯示名稱",
+ "helper": "此儲存庫的易讀名稱。"
+ },
+ "path": {
+ "label": "儲存庫路徑"
+ }
+ },
"scripts": {
"title": "腳本與設定",
- "description": "為此專案設定設定腳本、清理腳本與要複製的檔案。",
+ "description": "設定此儲存庫的設定腳本、清理腳本與要複製的檔案。這些腳本在儲存庫用於任何工作區時執行。",
"setup": {
"label": "設定腳本",
"helper": "此腳本在工作樹內執行,於建立後、編碼代理啟動前執行。用於安裝相依套件或準備環境等設定工作。",
"parallelLabel": "與編碼代理平行執行設定腳本",
"parallelHelper": "啟用後,設定腳本將與編碼代理同時執行,而不是等待設定完成後再啟動。"
},
- "dev": {
- "label": "開發伺服器腳本",
- "helper": "從任務嘗試中啟動開發伺服器。腳本會從工作區目錄(每個儲存庫工作樹的上一層)執行,除非在下方指定工作目錄。"
- },
- "devWorkingDir": {
- "label": "開發伺服器工作目錄",
- "placeholder": "例如:my-repo",
- "helper": "執行開發伺服器腳本的目錄,相對於工作區根目錄。留空則從工作區根目錄執行。"
- },
- "agentWorkingDir": {
- "label": "代理工作目錄",
- "placeholder": "例如:my-repo",
- "helper": "新工作區執行編碼代理的預設目錄,相對於工作區根目錄。此值在建立工作區時保存,不會影響現有工作區。單一儲存庫專案預設為儲存庫名稱。留空則從工作區根目錄執行。"
- },
"cleanup": {
"label": "清理腳本",
- "helper": "此腳本在工作樹內執行,於編碼代理執行後(僅在有變更時)執行。用於品質保證工作,如執行 linter、格式化工具、測試或其他驗證步驟。若無變更將略過此腳本。"
+ "helper": "此腳本在工作樹內執行,於編碼代理執行後(僅在有變更時)執行。用於品質保證工作,如執行 linter、格式化工具、測試或其他驗證步驟。"
},
"copyFiles": {
"label": "複製檔案",
- "helper": "要從原始專案目錄複製到工作樹的檔案清單(以逗號分隔)。這些檔案會在建立工作樹後但在執行設定腳本前複製。適合用於 .env、設定檔與本機設定等環境特定檔案。請確保這些檔案已加入 gitignore,否則可能會被提交!"
+ "helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore!"
+ },
+ "devServer": {
+ "label": "開發伺服器腳本",
+ "helper": "啟動此儲存庫的開發伺服器。腳本會從儲存庫的工作樹目錄執行。"
}
},
"save": {
- "button": "儲存專案設定",
- "success": "✓ 專案設定儲存成功!",
- "error": "儲存專案設定失敗",
- "unsavedChanges": "• 您有未儲存的變更",
+ "button": "儲存儲存庫設定",
+ "success": "儲存庫設定儲存成功!",
+ "error": "儲存儲存庫設定失敗",
+ "unsavedChanges": "您有未儲存的變更",
"discard": "放棄",
- "confirmSwitch": "您有未儲存的變更。確定要切換專案嗎?您的變更將會遺失。"
+ "confirmSwitch": "您有未儲存的變更。確定要切換儲存庫嗎?您的變更將會遺失。"
}
}
},
diff --git a/frontend/src/i18n/locales/zh-Hant/tasks.json b/frontend/src/i18n/locales/zh-Hant/tasks.json
index fe7e350564..d20a956f85 100644
--- a/frontend/src/i18n/locales/zh-Hant/tasks.json
+++ b/frontend/src/i18n/locales/zh-Hant/tasks.json
@@ -101,7 +101,7 @@
"companionPrompt": "要使用點擊編輯功能,請將瀏覽器 Companion 加到您的專案。",
"companionLink": "查看安裝指南",
"startButton": "啟動開發伺服器",
- "editButton": "編輯開發腳本",
+ "configureButton": "設定",
"stopAndEditButton": "停止開發伺服器並解決問題"
},
"devScript": {
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 152358a322..8419dcca24 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -17,12 +17,11 @@ import {
ExecutionProcessRepoState,
GitBranch,
Project,
- ProjectRepo,
Repo,
RepoWithTargetBranch,
CreateProject,
CreateProjectRepo,
- UpdateProjectRepo,
+ UpdateRepo,
SearchResult,
ShareTaskResponse,
Task,
@@ -358,31 +357,6 @@ export const projectsApi = {
);
return handleApiResponse(response);
},
-
- getRepository: async (
- projectId: string,
- repoId: string
- ): Promise => {
- const response = await makeRequest(
- `/api/projects/${projectId}/repositories/${repoId}`
- );
- return handleApiResponse(response);
- },
-
- updateRepository: async (
- projectId: string,
- repoId: string,
- data: UpdateProjectRepo
- ): Promise => {
- const response = await makeRequest(
- `/api/projects/${projectId}/repositories/${repoId}`,
- {
- method: 'PUT',
- body: JSON.stringify(data),
- }
- );
- return handleApiResponse(response);
- },
};
// Task Management APIs
@@ -845,6 +819,24 @@ export const fileSystemApi = {
// Repo APIs
export const repoApi = {
+ list: async (): Promise => {
+ const response = await makeRequest('/api/repos');
+ return handleApiResponse(response);
+ },
+
+ getById: async (repoId: string): Promise => {
+ const response = await makeRequest(`/api/repos/${repoId}`);
+ return handleApiResponse(response);
+ },
+
+ update: async (repoId: string, data: UpdateRepo): Promise => {
+ const response = await makeRequest(`/api/repos/${repoId}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ });
+ return handleApiResponse(response);
+ },
+
register: async (data: {
path: string;
display_name?: string;
diff --git a/frontend/src/lib/devServerUtils.ts b/frontend/src/lib/devServerUtils.ts
new file mode 100644
index 0000000000..ddd02e1a79
--- /dev/null
+++ b/frontend/src/lib/devServerUtils.ts
@@ -0,0 +1,56 @@
+import type { ExecutionProcess } from 'shared/types';
+
+/**
+ * Extract the working directory from a dev server process's executor action.
+ */
+export function getDevServerWorkingDir(
+ process: ExecutionProcess
+): string | null {
+ const typ = process.executor_action?.typ;
+ if (typ && 'type' in typ && typ.type === 'ScriptRequest') {
+ return (typ as { working_dir: string | null }).working_dir;
+ }
+ return null;
+}
+
+/**
+ * Deduplicate dev server processes by working directory, keeping the latest
+ * process for each unique directory.
+ */
+export function deduplicateDevServersByWorkingDir(
+ processes: ExecutionProcess[]
+): ExecutionProcess[] {
+ const byWorkingDir = new Map();
+ for (const process of processes) {
+ const workingDir = getDevServerWorkingDir(process) ?? 'unknown';
+ const existing = byWorkingDir.get(workingDir);
+ if (
+ !existing ||
+ new Date(process.started_at) > new Date(existing.started_at)
+ ) {
+ byWorkingDir.set(workingDir, process);
+ }
+ }
+ return Array.from(byWorkingDir.values());
+}
+
+/**
+ * Filter processes to only include dev servers.
+ */
+export function filterDevServerProcesses(
+ processes: ExecutionProcess[]
+): ExecutionProcess[] {
+ return processes.filter((process) => process.run_reason === 'devserver');
+}
+
+/**
+ * Filter processes to only include running dev servers.
+ */
+export function filterRunningDevServers(
+ processes: ExecutionProcess[]
+): ExecutionProcess[] {
+ return processes.filter(
+ (process) =>
+ process.run_reason === 'devserver' && process.status === 'running'
+ );
+}
diff --git a/frontend/src/pages/settings/ProjectSettings.tsx b/frontend/src/pages/settings/ProjectSettings.tsx
index 6e28c897b7..a116930d8a 100644
--- a/frontend/src/pages/settings/ProjectSettings.tsx
+++ b/frontend/src/pages/settings/ProjectSettings.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { useSearchParams } from 'react-router-dom';
+import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
import { isEqual } from 'lodash';
@@ -20,55 +20,30 @@ 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, Plus, Trash2 } from 'lucide-react';
import { useProjects } from '@/hooks/useProjects';
import { useProjectMutations } from '@/hooks/useProjectMutations';
-import { useScriptPlaceholders } from '@/hooks/useScriptPlaceholders';
-import { CopyFilesField } from '@/components/projects/CopyFilesField';
-import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea';
import { RepoPickerDialog } from '@/components/dialogs/shared/RepoPickerDialog';
import { projectsApi } from '@/lib/api';
import { repoBranchKeys } from '@/hooks/useRepoBranches';
-import type { Project, ProjectRepo, Repo, UpdateProject } from 'shared/types';
+import type { Project, Repo, UpdateProject } from 'shared/types';
interface ProjectFormState {
name: string;
- dev_script: string;
- dev_script_working_dir: string;
default_agent_working_dir: string;
}
-interface RepoScriptsFormState {
- setup_script: string;
- parallel_setup_script: boolean;
- cleanup_script: string;
- copy_files: string;
-}
-
function projectToFormState(project: Project): ProjectFormState {
return {
name: project.name,
- dev_script: project.dev_script ?? '',
- dev_script_working_dir: project.dev_script_working_dir ?? '',
default_agent_working_dir: project.default_agent_working_dir ?? '',
};
}
-function projectRepoToScriptsFormState(
- projectRepo: ProjectRepo | null
-): RepoScriptsFormState {
- return {
- setup_script: projectRepo?.setup_script ?? '',
- parallel_setup_script: projectRepo?.parallel_setup_script ?? false,
- cleanup_script: projectRepo?.cleanup_script ?? '',
- copy_files: projectRepo?.copy_files ?? '',
- };
-}
-
export function ProjectSettings() {
const [searchParams, setSearchParams] = useSearchParams();
+ const navigate = useNavigate();
const projectIdParam = searchParams.get('projectId') ?? '';
const { t } = useTranslation('settings');
const queryClient = useQueryClient();
@@ -99,42 +74,12 @@ export function ProjectSettings() {
const [addingRepo, setAddingRepo] = useState(false);
const [deletingRepoId, setDeletingRepoId] = useState(null);
- // Scripts repo state (per-repo scripts)
- const [selectedScriptsRepoId, setSelectedScriptsRepoId] = useState<
- string | null
- >(null);
- const [selectedProjectRepo, setSelectedProjectRepo] =
- useState(null);
- const [scriptsDraft, setScriptsDraft] = useState(
- null
- );
- const [loadingProjectRepo, setLoadingProjectRepo] = useState(false);
- const [savingScripts, setSavingScripts] = useState(false);
- const [scriptsSuccess, setScriptsSuccess] = useState(false);
- const [scriptsError, setScriptsError] = useState(null);
-
- // Get OS-appropriate script placeholders
- const placeholders = useScriptPlaceholders();
-
// Check for unsaved changes (project name)
- const hasUnsavedProjectChanges = useMemo(() => {
+ const hasUnsavedChanges = useMemo(() => {
if (!draft || !selectedProject) return false;
return !isEqual(draft, projectToFormState(selectedProject));
}, [draft, selectedProject]);
- // Check for unsaved script changes
- const hasUnsavedScriptsChanges = useMemo(() => {
- if (!scriptsDraft || !selectedProjectRepo) return false;
- return !isEqual(
- scriptsDraft,
- projectRepoToScriptsFormState(selectedProjectRepo)
- );
- }, [scriptsDraft, selectedProjectRepo]);
-
- // Combined check for any unsaved changes
- const hasUnsavedChanges =
- hasUnsavedProjectChanges || hasUnsavedScriptsChanges;
-
// Handle project selection from dropdown
const handleProjectSelect = useCallback(
(id: string) => {
@@ -256,56 +201,6 @@ export function ProjectSettings() {
.finally(() => setLoadingRepos(false));
}, [selectedProjectId]);
- // Auto-select first repository for scripts when repositories load
- useEffect(() => {
- if (repositories.length > 0 && !selectedScriptsRepoId) {
- setSelectedScriptsRepoId(repositories[0].id);
- }
- // Clear selection if repo was deleted
- if (
- selectedScriptsRepoId &&
- !repositories.some((r) => r.id === selectedScriptsRepoId)
- ) {
- setSelectedScriptsRepoId(repositories[0]?.id ?? null);
- }
- }, [repositories, selectedScriptsRepoId]);
-
- // Reset scripts selection when project changes
- useEffect(() => {
- setSelectedScriptsRepoId(null);
- setSelectedProjectRepo(null);
- setScriptsDraft(null);
- setScriptsError(null);
- }, [selectedProjectId]);
-
- // Fetch ProjectRepo scripts when selected scripts repo changes
- useEffect(() => {
- if (!selectedProjectId || !selectedScriptsRepoId) {
- setSelectedProjectRepo(null);
- setScriptsDraft(null);
- return;
- }
-
- setLoadingProjectRepo(true);
- setScriptsError(null);
- projectsApi
- .getRepository(selectedProjectId, selectedScriptsRepoId)
- .then((projectRepo) => {
- setSelectedProjectRepo(projectRepo);
- setScriptsDraft(projectRepoToScriptsFormState(projectRepo));
- })
- .catch((err) => {
- setScriptsError(
- err instanceof Error
- ? err.message
- : 'Failed to load repository scripts'
- );
- setSelectedProjectRepo(null);
- setScriptsDraft(null);
- })
- .finally(() => setLoadingProjectRepo(false));
- }, [selectedProjectId, selectedScriptsRepoId]);
-
const handleAddRepository = async () => {
if (!selectedProjectId) return;
@@ -331,6 +226,9 @@ export function ProjectSettings() {
queryClient.invalidateQueries({
queryKey: ['projectRepositories', selectedProjectId],
});
+ queryClient.invalidateQueries({
+ queryKey: ['repos'],
+ });
queryClient.invalidateQueries({
queryKey: repoBranchKeys.byRepo(newRepo.id),
});
@@ -354,6 +252,9 @@ export function ProjectSettings() {
queryClient.invalidateQueries({
queryKey: ['projectRepositories', selectedProjectId],
});
+ queryClient.invalidateQueries({
+ queryKey: ['repos'],
+ });
queryClient.invalidateQueries({
queryKey: repoBranchKeys.byRepo(repoId),
});
@@ -393,8 +294,6 @@ export function ProjectSettings() {
try {
const updateData: UpdateProject = {
name: draft.name.trim(),
- dev_script: draft.dev_script.trim() || null,
- dev_script_working_dir: draft.dev_script_working_dir.trim() || null,
default_agent_working_dir:
draft.default_agent_working_dir.trim() || null,
};
@@ -410,47 +309,11 @@ export function ProjectSettings() {
}
};
- const handleSaveScripts = async () => {
- if (!scriptsDraft || !selectedProjectId || !selectedScriptsRepoId) return;
-
- setSavingScripts(true);
- setScriptsError(null);
- setScriptsSuccess(false);
-
- try {
- const updatedRepo = await projectsApi.updateRepository(
- selectedProjectId,
- selectedScriptsRepoId,
- {
- setup_script: scriptsDraft.setup_script.trim() || null,
- cleanup_script: scriptsDraft.cleanup_script.trim() || null,
- copy_files: scriptsDraft.copy_files.trim() || null,
- parallel_setup_script: scriptsDraft.parallel_setup_script,
- }
- );
- setSelectedProjectRepo(updatedRepo);
- setScriptsDraft(projectRepoToScriptsFormState(updatedRepo));
- setScriptsSuccess(true);
- setTimeout(() => setScriptsSuccess(false), 3000);
- } catch (err) {
- setScriptsError(
- err instanceof Error ? err.message : 'Failed to save scripts'
- );
- } finally {
- setSavingScripts(false);
- }
- };
-
const handleDiscard = () => {
if (!selectedProject) return;
setDraft(projectToFormState(selectedProject));
};
- const handleDiscardScripts = () => {
- if (!selectedProjectRepo) return;
- setScriptsDraft(projectRepoToScriptsFormState(selectedProjectRepo));
- };
-
const updateDraft = (updates: Partial) => {
setDraft((prev) => {
if (!prev) return prev;
@@ -458,13 +321,6 @@ export function ProjectSettings() {
});
};
- const updateScriptsDraft = (updates: Partial) => {
- setScriptsDraft((prev) => {
- if (!prev) return prev;
- return { ...prev, ...updates };
- });
- };
-
if (projectsLoading) {
return (
@@ -573,46 +429,9 @@ export function ProjectSettings() {
-
-
- {t('settings.projects.scripts.dev.label')}
-
-
updateDraft({ dev_script: e.target.value })}
- placeholder={placeholders.dev}
- maxRows={12}
- className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
- />
-
- {t('settings.projects.scripts.dev.helper')}
-
-
-
-
-
- {t('settings.projects.scripts.devWorkingDir.label')}
-
-
- updateDraft({ dev_script_working_dir: e.target.value })
- }
- placeholder={t(
- 'settings.projects.scripts.devWorkingDir.placeholder'
- )}
- className="font-mono"
- />
-
- {t('settings.projects.scripts.devWorkingDir.helper')}
-
-
-
{/* Save Button */}
- {hasUnsavedProjectChanges ? (
+ {hasUnsavedChanges ? (
{t('settings.projects.save.unsavedChanges')}
@@ -643,13 +462,13 @@ export function ProjectSettings() {
{t('settings.projects.save.discard')}
{saving ? (
<>
@@ -662,18 +481,6 @@ export function ProjectSettings() {
- {error && (
-
- {error}
-
- )}
- {success && (
-
-
- {t('settings.projects.save.success')}
-
-
- )}
@@ -704,7 +511,10 @@ export function ProjectSettings() {
{repositories.map((repo) => (
+ navigate(`/settings/repos?repoId=${repo.id}`)
+ }
>
{repo.display_name}
@@ -715,7 +525,10 @@ export function ProjectSettings() {
handleDeleteRepository(repo.id)}
+ onClick={(e) => {
+ e.stopPropagation();
+ handleDeleteRepository(repo.id);
+ }}
disabled={deletingRepoId === repo.id}
title="Delete repository"
>
@@ -753,185 +566,8 @@ export function ProjectSettings() {
-
-
- {t('settings.projects.scripts.title')}
-
- {t('settings.projects.scripts.description')}
-
-
-
- {scriptsError && (
-
- {scriptsError}
-
- )}
-
- {scriptsSuccess && (
-
-
- Scripts saved successfully
-
-
- )}
-
- {repositories.length === 0 ? (
-
- Add a repository above to configure scripts
-
- ) : (
- <>
- {/* Repository Selector for Scripts */}
-
-
Repository
-
-
-
-
-
- {repositories.map((repo) => (
-
- {repo.display_name}
-
- ))}
-
-
-
- Configure scripts for each repository separately
-
-
-
- {loadingProjectRepo ? (
-
-
-
- Loading scripts...
-
-
- ) : scriptsDraft ? (
- <>
-
-
- {t('settings.projects.scripts.setup.label')}
-
-
- updateScriptsDraft({ setup_script: e.target.value })
- }
- placeholder={placeholders.setup}
- maxRows={12}
- className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
- />
-
- {t('settings.projects.scripts.setup.helper')}
-
-
-
-
- updateScriptsDraft({
- parallel_setup_script: checked === true,
- })
- }
- disabled={!scriptsDraft.setup_script.trim()}
- />
-
- {t('settings.projects.scripts.setup.parallelLabel')}
-
-
-
- {t('settings.projects.scripts.setup.parallelHelper')}
-
-
-
-
-
- {t('settings.projects.scripts.cleanup.label')}
-
-
- updateScriptsDraft({
- cleanup_script: e.target.value,
- })
- }
- placeholder={placeholders.cleanup}
- maxRows={12}
- className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
- />
-
- {t('settings.projects.scripts.cleanup.helper')}
-
-
-
-
-
- {t('settings.projects.scripts.copyFiles.label')}
-
-
- updateScriptsDraft({ copy_files: value })
- }
- projectId={selectedProject.id}
- />
-
- {t('settings.projects.scripts.copyFiles.helper')}
-
-
-
- {/* Scripts Save Buttons */}
-
- {hasUnsavedScriptsChanges ? (
-
- {t('settings.projects.save.unsavedChanges')}
-
- ) : (
-
- )}
-
-
- {t('settings.projects.save.discard')}
-
-
- {savingScripts && (
-
- )}
- Save Scripts
-
-
-
- >
- ) : null}
- >
- )}
-
-
-
{/* Sticky Save Button for Project Name */}
- {hasUnsavedProjectChanges && (
+ {hasUnsavedChanges && (
diff --git a/frontend/src/pages/settings/ReposSettings.tsx b/frontend/src/pages/settings/ReposSettings.tsx
new file mode 100644
index 0000000000..d0945e09b2
--- /dev/null
+++ b/frontend/src/pages/settings/ReposSettings.tsx
@@ -0,0 +1,472 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { isEqual } from 'lodash';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} 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 } from 'lucide-react';
+import { useScriptPlaceholders } from '@/hooks/useScriptPlaceholders';
+import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea';
+import { repoApi } from '@/lib/api';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import type { Repo, UpdateRepo } from 'shared/types';
+
+interface RepoScriptsFormState {
+ display_name: string;
+ setup_script: string;
+ parallel_setup_script: boolean;
+ cleanup_script: string;
+ copy_files: string;
+ dev_server_script: string;
+}
+
+function repoToFormState(repo: Repo): RepoScriptsFormState {
+ return {
+ display_name: repo.display_name,
+ setup_script: repo.setup_script ?? '',
+ parallel_setup_script: repo.parallel_setup_script,
+ cleanup_script: repo.cleanup_script ?? '',
+ copy_files: repo.copy_files ?? '',
+ dev_server_script: repo.dev_server_script ?? '',
+ };
+}
+
+export function ReposSettings() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const repoIdParam = searchParams.get('repoId') ?? '';
+ const { t } = useTranslation('settings');
+ const queryClient = useQueryClient();
+
+ // Fetch all repos
+ const {
+ data: repos,
+ isLoading: reposLoading,
+ error: reposError,
+ } = useQuery({
+ queryKey: ['repos'],
+ queryFn: () => repoApi.list(),
+ });
+
+ // Selected repo state
+ const [selectedRepoId, setSelectedRepoId] = useState(repoIdParam);
+ const [selectedRepo, setSelectedRepo] = useState(null);
+
+ // Form state
+ const [draft, setDraft] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+
+ // Get OS-appropriate script placeholders
+ const placeholders = useScriptPlaceholders();
+
+ // Check for unsaved changes
+ const hasUnsavedChanges = useMemo(() => {
+ if (!draft || !selectedRepo) return false;
+ return !isEqual(draft, repoToFormState(selectedRepo));
+ }, [draft, selectedRepo]);
+
+ // Handle repo selection from dropdown
+ const handleRepoSelect = useCallback(
+ (id: string) => {
+ if (id === selectedRepoId) return;
+
+ if (hasUnsavedChanges) {
+ const confirmed = window.confirm(
+ t('settings.repos.save.confirmSwitch')
+ );
+ if (!confirmed) return;
+ setDraft(null);
+ setSelectedRepo(null);
+ setSuccess(false);
+ setError(null);
+ }
+
+ setSelectedRepoId(id);
+ if (id) {
+ setSearchParams({ repoId: id });
+ } else {
+ setSearchParams({});
+ }
+ },
+ [hasUnsavedChanges, selectedRepoId, setSearchParams, t]
+ );
+
+ // Sync selectedRepoId when URL changes
+ useEffect(() => {
+ if (repoIdParam === selectedRepoId) return;
+
+ if (hasUnsavedChanges) {
+ const confirmed = window.confirm(t('settings.repos.save.confirmSwitch'));
+ if (!confirmed) {
+ if (selectedRepoId) {
+ setSearchParams({ repoId: selectedRepoId });
+ } else {
+ setSearchParams({});
+ }
+ return;
+ }
+ setDraft(null);
+ setSelectedRepo(null);
+ setSuccess(false);
+ setError(null);
+ }
+
+ setSelectedRepoId(repoIdParam);
+ }, [repoIdParam, hasUnsavedChanges, selectedRepoId, setSearchParams, t]);
+
+ // Populate draft from server data
+ useEffect(() => {
+ if (!repos) return;
+
+ const nextRepo = selectedRepoId
+ ? repos.find((r) => r.id === selectedRepoId)
+ : null;
+
+ setSelectedRepo((prev) =>
+ prev?.id === nextRepo?.id ? prev : (nextRepo ?? null)
+ );
+
+ if (!nextRepo) {
+ if (!hasUnsavedChanges) setDraft(null);
+ return;
+ }
+
+ if (hasUnsavedChanges) return;
+
+ setDraft(repoToFormState(nextRepo));
+ }, [repos, selectedRepoId, hasUnsavedChanges]);
+
+ // Warn on tab close/navigation with unsaved changes
+ useEffect(() => {
+ const handler = (e: BeforeUnloadEvent) => {
+ if (hasUnsavedChanges) {
+ e.preventDefault();
+ e.returnValue = '';
+ }
+ };
+ window.addEventListener('beforeunload', handler);
+ return () => window.removeEventListener('beforeunload', handler);
+ }, [hasUnsavedChanges]);
+
+ const handleSave = async () => {
+ if (!draft || !selectedRepo) return;
+
+ setSaving(true);
+ setError(null);
+ setSuccess(false);
+
+ try {
+ const updateData: UpdateRepo = {
+ display_name: draft.display_name.trim() || null,
+ setup_script: draft.setup_script.trim() || null,
+ cleanup_script: draft.cleanup_script.trim() || null,
+ copy_files: draft.copy_files.trim() || null,
+ parallel_setup_script: draft.parallel_setup_script,
+ dev_server_script: draft.dev_server_script.trim() || null,
+ };
+
+ const updatedRepo = await repoApi.update(selectedRepo.id, updateData);
+ setSelectedRepo(updatedRepo);
+ setDraft(repoToFormState(updatedRepo));
+ queryClient.setQueryData(['repos'], (old: Repo[] | undefined) =>
+ old?.map((r) => (r.id === updatedRepo.id ? updatedRepo : r))
+ );
+ setSuccess(true);
+ setTimeout(() => setSuccess(false), 3000);
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : t('settings.repos.save.error')
+ );
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleDiscard = () => {
+ if (!selectedRepo) return;
+ setDraft(repoToFormState(selectedRepo));
+ };
+
+ const updateDraft = (updates: Partial) => {
+ setDraft((prev) => {
+ if (!prev) return prev;
+ return { ...prev, ...updates };
+ });
+ };
+
+ if (reposLoading) {
+ return (
+
+
+ {t('settings.repos.loading')}
+
+ );
+ }
+
+ if (reposError) {
+ return (
+
+
+
+ {reposError instanceof Error
+ ? reposError.message
+ : t('settings.repos.loadError')}
+
+
+
+ );
+ }
+
+ return (
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {success && (
+
+
+ {t('settings.repos.save.success')}
+
+
+ )}
+
+
+
+ {t('settings.repos.title')}
+ {t('settings.repos.description')}
+
+
+
+
+ {t('settings.repos.selector.label')}
+
+
+
+
+
+
+ {repos && repos.length > 0 ? (
+ repos.map((repo) => (
+
+ {repo.display_name}
+
+ ))
+ ) : (
+
+ {t('settings.repos.selector.noRepos')}
+
+ )}
+
+
+
+ {t('settings.repos.selector.helper')}
+
+
+
+
+
+ {selectedRepo && draft && (
+ <>
+
+
+ {t('settings.repos.general.title')}
+
+ {t('settings.repos.general.description')}
+
+
+
+
+
+ {t('settings.repos.general.displayName.label')}
+
+
+ updateDraft({ display_name: e.target.value })
+ }
+ placeholder={t(
+ 'settings.repos.general.displayName.placeholder'
+ )}
+ />
+
+ {t('settings.repos.general.displayName.helper')}
+
+
+
+
+
{t('settings.repos.general.path.label')}
+
+ {selectedRepo.path}
+
+
+
+
+
+
+
+ {t('settings.repos.scripts.title')}
+
+ {t('settings.repos.scripts.description')}
+
+
+
+
+
+ {t('settings.repos.scripts.setup.label')}
+
+
+ updateDraft({ setup_script: e.target.value })
+ }
+ placeholder={placeholders.setup}
+ maxRows={12}
+ className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
+ />
+
+ {t('settings.repos.scripts.setup.helper')}
+
+
+
+
+ updateDraft({
+ parallel_setup_script: checked === true,
+ })
+ }
+ disabled={!draft.setup_script.trim()}
+ />
+
+ {t('settings.repos.scripts.setup.parallelLabel')}
+
+
+
+ {t('settings.repos.scripts.setup.parallelHelper')}
+
+
+
+
+
+ {t('settings.repos.scripts.cleanup.label')}
+
+
+ updateDraft({
+ cleanup_script: e.target.value,
+ })
+ }
+ placeholder={placeholders.cleanup}
+ maxRows={12}
+ className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
+ />
+
+ {t('settings.repos.scripts.cleanup.helper')}
+
+
+
+
+
+ {t('settings.repos.scripts.copyFiles.label')}
+
+
updateDraft({ copy_files: e.target.value })}
+ placeholder=".env, .env.local"
+ maxRows={6}
+ className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
+ />
+
+ {t('settings.repos.scripts.copyFiles.helper')}
+
+
+
+
+
+ {t('settings.repos.scripts.devServer.label')}
+
+
+ updateDraft({
+ dev_server_script: e.target.value,
+ })
+ }
+ placeholder={placeholders.dev}
+ maxRows={12}
+ className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono"
+ />
+
+ {t('settings.repos.scripts.devServer.helper')}
+
+
+
+ {/* Save Buttons */}
+
+ {hasUnsavedChanges ? (
+
+ {t('settings.repos.save.unsavedChanges')}
+
+ ) : (
+
+ )}
+
+
+ {t('settings.repos.save.discard')}
+
+
+ {saving && (
+
+ )}
+ {t('settings.repos.save.button')}
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/settings/SettingsLayout.tsx b/frontend/src/pages/settings/SettingsLayout.tsx
index 41987487c9..aef40cc315 100644
--- a/frontend/src/pages/settings/SettingsLayout.tsx
+++ b/frontend/src/pages/settings/SettingsLayout.tsx
@@ -1,6 +1,14 @@
import { NavLink, Outlet } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Settings, Cpu, Server, X, FolderOpen, Building2 } from 'lucide-react';
+import {
+ Settings,
+ Cpu,
+ Server,
+ X,
+ FolderOpen,
+ Building2,
+ GitBranch,
+} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { useEffect } from 'react';
@@ -18,6 +26,10 @@ const settingsNavigation = [
path: 'projects',
icon: FolderOpen,
},
+ {
+ path: 'repos',
+ icon: GitBranch,
+ },
{
path: 'organizations',
icon: Building2,
diff --git a/frontend/src/pages/settings/index.ts b/frontend/src/pages/settings/index.ts
index 63548fdd6c..37ef33b3c2 100644
--- a/frontend/src/pages/settings/index.ts
+++ b/frontend/src/pages/settings/index.ts
@@ -1,6 +1,7 @@
export { SettingsLayout } from './SettingsLayout';
export { GeneralSettings } from './GeneralSettings';
export { ProjectSettings } from './ProjectSettings';
+export { ReposSettings } from './ReposSettings';
export { OrganizationSettings } from './OrganizationSettings';
export { AgentSettings } from './AgentSettings';
export { McpSettings } from './McpSettings';
diff --git a/shared/types.ts b/shared/types.ts
index 7c3004ebb8..4c4db60df6 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, dev_script: string | null, dev_script_working_dir: string | null, default_agent_working_dir: string | null, remote_project_id: string | null, created_at: Date, updated_at: Date, };
+export type Project = { id: string, name: string, default_agent_working_dir: string | null, remote_project_id: string | null, created_at: Date, updated_at: Date, };
export type CreateProject = { name: string, repositories: Array, };
-export type UpdateProject = { name: string | null, dev_script: string | null, dev_script_working_dir: string | null, default_agent_working_dir: string | null, };
+export type UpdateProject = { name: string | null, default_agent_working_dir: string | null, };
export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType,
/**
@@ -26,19 +26,19 @@ score: bigint, };
export type SearchMatchType = "FileName" | "DirectoryName" | "FullPath";
-export type Repo = { id: string, path: string, name: string, display_name: string, created_at: Date, updated_at: Date, };
+export type Repo = { id: string, path: string, name: string, display_name: string, setup_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean, dev_server_script: string | null, created_at: Date, updated_at: Date, };
-export type ProjectRepo = { id: string, project_id: string, repo_id: string, setup_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean, };
+export type UpdateRepo = { display_name: string | null, setup_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean | null, dev_server_script: string | null, };
-export type CreateProjectRepo = { display_name: string, git_repo_path: string, };
+export type ProjectRepo = { id: string, project_id: string, repo_id: string, };
-export type UpdateProjectRepo = { setup_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean | null, };
+export type CreateProjectRepo = { display_name: string, git_repo_path: string, };
export type WorkspaceRepo = { id: string, workspace_id: string, repo_id: string, target_branch: string, created_at: Date, updated_at: Date, };
export type CreateWorkspaceRepo = { repo_id: string, target_branch: string, };
-export type RepoWithTargetBranch = { target_branch: string, id: string, path: string, name: string, display_name: string, created_at: Date, updated_at: Date, };
+export type RepoWithTargetBranch = { target_branch: string, id: string, path: string, name: string, display_name: string, setup_script: string | null, cleanup_script: string | null, copy_files: string | null, parallel_setup_script: boolean, dev_server_script: string | null, created_at: Date, updated_at: Date, };
export type Tag = { id: string, tag_name: string, content: string, created_at: string, updated_at: string, };
@@ -284,8 +284,6 @@ conflicted_files: Array, };
export type RunScriptError = { "type": "no_script_configured" } | { "type": "process_already_running" };
-export type DeleteWorkspaceError = { "type": "has_running_processes" };
-
export type AttachPrResponse = { pr_attached: boolean, pr_url: string | null, pr_number: bigint | null, pr_status: MergeStatus | null, };
export type AttachExistingPrRequest = { repo_id: string, };