Squashed commit of the following:
commit ca21aa40163902dfb20582d6dced8c884b4b0119 Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 16:50:43 2025 +0100 Fixes commit 75c982209a71704d0df15982b9ac0aca87aa68de Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 16:35:58 2025 +0100 Improve process killing commit f58fd3b8a315880cc940d7e59719d23428c72e92 Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 16:23:59 2025 +0100 WIP commit 7a6cd4772e15a5df0d760fe79776979c3ba206e8 Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 12:34:13 2025 +0100 Fix dev server activity not showing commit 09eb3095c1850b5f3173b72b6b220811ef68524c Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 12:27:01 2025 +0100 Add activity for dev server commit 73db9a20312a8ed15c130760c6aacfa720d102d7 Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 12:04:38 2025 +0100 Lint commit 0a0ad901773e14f634ded8a68a108efc2fbca0ae Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 24 12:01:37 2025 +0100 WIP dev server
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects WHERE git_repo_path = $1",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, dev_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects WHERE git_repo_path = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,14 +24,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "dev_script",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -42,9 +47,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b62fa26fe7cdbee672504dbf63d3dbe19fca02a4a4f97d7df7143f340540efa0"
|
||||
"hash": "056991f6ec992103f9de72475138ddfa8d5c9d42546fe36116a61f4db94611c3"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects ORDER BY created_at DESC",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, dev_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,14 +24,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "dev_script",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -42,9 +47,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "420c9eec0dd98062947b090bc695b67c2bcaba9862c06b701a9ba3d8a5b02abf"
|
||||
"hash": "08f2cb03665a16640d6690f29920521bae3479e3d2602e724d2c93e6fc85d8ee"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects WHERE git_repo_path = $1 AND id != $2",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, dev_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects WHERE git_repo_path = $1 AND id != $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,14 +24,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "dev_script",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -42,9 +47,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "205da45211b3aa413684ecd76d065fc59f793da42da075246464ac776016f5ff"
|
||||
"hash": "1f3dd0f80e984a8472457be40cd96e1a03de71cc6c8adc62ef4873b79449f078"
|
||||
}
|
||||
104
backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json
generated
Normal file
104
backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json
generated
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT \n ep.id as \"id!: Uuid\", \n ep.task_attempt_id as \"task_attempt_id!: Uuid\", \n ep.process_type as \"process_type!: ExecutionProcessType\",\n ep.executor_type,\n ep.status as \"status!: ExecutionProcessStatus\",\n ep.command, \n ep.args, \n ep.working_directory, \n ep.stdout, \n ep.stderr, \n ep.exit_code,\n ep.started_at as \"started_at!: DateTime<Utc>\",\n ep.completed_at as \"completed_at?: DateTime<Utc>\",\n ep.created_at as \"created_at!: DateTime<Utc>\", \n ep.updated_at as \"updated_at!: DateTime<Utc>\"\n FROM execution_processes ep\n JOIN task_attempts ta ON ep.task_attempt_id = ta.id\n JOIN tasks t ON ta.task_id = t.id\n WHERE ep.status = 'running' \n AND ep.process_type = 'devserver'\n AND t.project_id = $1\n ORDER BY ep.created_at ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "task_attempt_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "process_type!: ExecutionProcessType",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "executor_type",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "status!: ExecutionProcessStatus",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "command",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "exit_code",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "started_at!: DateTime<Utc>",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "completed_at?: DateTime<Utc>",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE projects SET name = $2, git_repo_path = $3, setup_script = $4 WHERE id = $1 RETURNING id as \"id!: Uuid\", name, git_repo_path, setup_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"query": "UPDATE projects SET name = $2, git_repo_path = $3, setup_script = $4, dev_script = $5 WHERE id = $1 RETURNING id as \"id!: Uuid\", name, git_repo_path, setup_script, dev_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,27 +24,33 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "dev_script",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3bead952fd42b79bed0908db603726935c0e830ea74ff30064bac71185442fc"
|
||||
"hash": "42c0c81bb893af019b5b91b48c3cb65557f770894e21e654303047d4150cca93"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects WHERE id = $1",
|
||||
"query": "SELECT id as \"id!: Uuid\", name, git_repo_path, setup_script, dev_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\" FROM projects WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,14 +24,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "dev_script",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -42,9 +47,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "346d58b8e0628d6a5936675beadc0a43ffa2dca384ed4f4b3a3abfcd09592c07"
|
||||
"hash": "4fd26525fb4e2f606200695e1b62509409e3763fa6e6c8a905c5f9536b2c9a92"
|
||||
}
|
||||
92
backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json
generated
Normal file
92
backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json
generated
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT \n id as \"id!: Uuid\", \n task_attempt_id as \"task_attempt_id!: Uuid\", \n process_type as \"process_type!: ExecutionProcessType\",\n executor_type,\n status as \"status!: ExecutionProcessStatus\",\n command, \n args, \n working_directory, \n exit_code,\n started_at as \"started_at!: DateTime<Utc>\",\n completed_at as \"completed_at?: DateTime<Utc>\",\n created_at as \"created_at!: DateTime<Utc>\", \n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM execution_processes \n WHERE task_attempt_id = $1 \n ORDER BY created_at ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "task_attempt_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "process_type!: ExecutionProcessType",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "executor_type",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "status!: ExecutionProcessStatus",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "command",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "exit_code",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "started_at!: DateTime<Utc>",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "completed_at?: DateTime<Utc>",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO projects (id, name, git_repo_path, setup_script) VALUES ($1, $2, $3, $4) RETURNING id as \"id!: Uuid\", name, git_repo_path, setup_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"query": "INSERT INTO projects (id, name, git_repo_path, setup_script, dev_script) VALUES ($1, $2, $3, $4, $5) RETURNING id as \"id!: Uuid\", name, git_repo_path, setup_script, dev_script, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,27 +24,33 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "dev_script",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "64fd750d2f767096f94b28650018dc657ad41c6a0af908215f694100319b4864"
|
||||
"hash": "5dc5d9e57b9dee5421b414f385a4c99f6014c4d9c0f965ff571ec75945132285"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Add dev_script column to projects table
|
||||
ALTER TABLE projects ADD COLUMN dev_script TEXT DEFAULT '';
|
||||
@@ -103,6 +103,7 @@ fn main() {
|
||||
vibe_kanban::models::task_attempt::WorktreeDiff::decl(),
|
||||
vibe_kanban::models::task_attempt::BranchStatus::decl(),
|
||||
vibe_kanban::models::execution_process::ExecutionProcess::decl(),
|
||||
vibe_kanban::models::execution_process::ExecutionProcessSummary::decl(),
|
||||
vibe_kanban::models::execution_process::ExecutionProcessStatus::decl(),
|
||||
vibe_kanban::models::execution_process::ExecutionProcessType::decl(),
|
||||
vibe_kanban::models::execution_process::CreateExecutionProcess::decl(),
|
||||
|
||||
@@ -218,26 +218,19 @@ pub async fn execution_monitor(app_state: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned task attempts AFTER handling completions
|
||||
// Check for orphaned execution processes AFTER handling completions
|
||||
// Add a small delay to ensure completed processes are properly handled first
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
let running_process_ids =
|
||||
match TaskAttemptActivity::find_processes_with_latest_running_status(&app_state.db_pool)
|
||||
.await
|
||||
{
|
||||
let running_processes = match ExecutionProcess::find_running(&app_state.db_pool).await {
|
||||
Ok(processes) => processes,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to query running attempts: {}", e);
|
||||
tracing::error!("Failed to query running execution processes: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for process_id in running_process_ids {
|
||||
// Get the execution process to find the task attempt ID
|
||||
let task_attempt_id =
|
||||
match ExecutionProcess::find_by_id(&app_state.db_pool, process_id).await {
|
||||
Ok(Some(process)) => {
|
||||
for process in running_processes {
|
||||
// Additional check: if the process was recently updated, skip it
|
||||
// This prevents race conditions with recent completions
|
||||
let now = chrono::Utc::now();
|
||||
@@ -246,29 +239,46 @@ pub async fn execution_monitor(app_state: AppState) {
|
||||
// Process was updated within last 10 seconds, likely just completed
|
||||
tracing::debug!(
|
||||
"Skipping recently updated process {} (updated {} seconds ago)",
|
||||
process_id,
|
||||
process.id,
|
||||
time_since_update.num_seconds()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
process.task_attempt_id
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::error!("Execution process {} not found", process_id);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch execution process {}: {}", process_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Double-check that this task attempt is not currently running and hasn't just completed
|
||||
if !app_state.has_running_execution(task_attempt_id).await {
|
||||
// This is truly an orphaned task attempt - mark it as failed
|
||||
// Check if this process is not actually running in the app state
|
||||
if !app_state
|
||||
.has_running_execution(process.task_attempt_id)
|
||||
.await
|
||||
{
|
||||
// This is truly an orphaned execution process - mark it as failed
|
||||
tracing::info!(
|
||||
"Found orphaned execution process {} for task attempt {}",
|
||||
process.id,
|
||||
process.task_attempt_id
|
||||
);
|
||||
|
||||
// Update the execution process status first
|
||||
if let Err(e) = ExecutionProcess::update_completion(
|
||||
&app_state.db_pool,
|
||||
process.id,
|
||||
ExecutionProcessStatus::Failed,
|
||||
None, // No exit code for orphaned processes
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to update orphaned execution process {} status: {}",
|
||||
process.id,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create task attempt activity for non-dev server processes
|
||||
if process.process_type != ExecutionProcessType::DevServer {
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id: process_id,
|
||||
execution_process_id: process.id,
|
||||
status: Some(TaskAttemptStatus::ExecutorFailed),
|
||||
note: Some("Execution lost (server restart or crash)".to_string()),
|
||||
};
|
||||
@@ -285,17 +295,23 @@ pub async fn execution_monitor(app_state: AppState) {
|
||||
"Failed to create failed activity for orphaned process: {}",
|
||||
e
|
||||
);
|
||||
} else {
|
||||
tracing::info!("Marked orphaned execution process {} as failed", process_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get task attempt and task to access task_id and project_id for status update
|
||||
tracing::info!("Marked orphaned execution process {} as failed", process.id);
|
||||
|
||||
// Update task status to InReview for coding agent and setup script failures
|
||||
if matches!(
|
||||
process.process_type,
|
||||
ExecutionProcessType::CodingAgent | ExecutionProcessType::SetupScript
|
||||
) {
|
||||
if let Ok(Some(task_attempt)) =
|
||||
TaskAttempt::find_by_id(&app_state.db_pool, task_attempt_id).await
|
||||
TaskAttempt::find_by_id(&app_state.db_pool, process.task_attempt_id).await
|
||||
{
|
||||
if let Ok(Some(task)) =
|
||||
Task::find_by_id(&app_state.db_pool, task_attempt.task_id).await
|
||||
{
|
||||
// Update task status to InReview
|
||||
if let Err(e) = Task::update_status(
|
||||
&app_state.db_pool,
|
||||
task.id,
|
||||
@@ -518,11 +534,11 @@ async fn handle_coding_agent_completion(
|
||||
|
||||
/// Handle dev server completion (future functionality)
|
||||
async fn handle_dev_server_completion(
|
||||
_app_state: &AppState,
|
||||
app_state: &AppState,
|
||||
task_attempt_id: Uuid,
|
||||
_execution_process_id: Uuid,
|
||||
execution_process_id: Uuid,
|
||||
_execution_process: ExecutionProcess,
|
||||
_success: bool,
|
||||
success: bool,
|
||||
exit_code: Option<i64>,
|
||||
) {
|
||||
let exit_text = if let Some(code) = exit_code {
|
||||
@@ -537,6 +553,24 @@ async fn handle_dev_server_completion(
|
||||
exit_text
|
||||
);
|
||||
|
||||
// Dev servers might restart automatically or have different completion semantics
|
||||
// For now, just log the completion
|
||||
// Update execution process status instead of creating activity
|
||||
let process_status = if success {
|
||||
ExecutionProcessStatus::Completed
|
||||
} else {
|
||||
ExecutionProcessStatus::Failed
|
||||
};
|
||||
|
||||
if let Err(e) = ExecutionProcess::update_completion(
|
||||
&app_state.db_pool,
|
||||
execution_process_id,
|
||||
process_status,
|
||||
exit_code,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to update dev server execution process status: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ pub trait Executor: Send + Sync {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExecutorType {
|
||||
SetupScript(String),
|
||||
DevServer(String),
|
||||
CodingAgent(ExecutorConfig),
|
||||
FollowUpCodingAgent {
|
||||
config: ExecutorConfig,
|
||||
|
||||
44
backend/src/executors/dev_server.rs
Normal file
44
backend/src/executors/dev_server.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use async_trait::async_trait;
|
||||
use tokio::process::{Child, Command};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::executor::{Executor, ExecutorError};
|
||||
use crate::models::project::Project;
|
||||
use crate::models::task::Task;
|
||||
|
||||
/// Executor for running project dev server scripts
|
||||
pub struct DevServerExecutor {
|
||||
pub script: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Executor for DevServerExecutor {
|
||||
async fn spawn(
|
||||
&self,
|
||||
pool: &sqlx::SqlitePool,
|
||||
task_id: Uuid,
|
||||
worktree_path: &str,
|
||||
) -> Result<Child, ExecutorError> {
|
||||
// Validate the task and project exist
|
||||
let task = Task::find_by_id(pool, task_id)
|
||||
.await?
|
||||
.ok_or(ExecutorError::TaskNotFound)?;
|
||||
|
||||
let _project = Project::find_by_id(pool, task.project_id)
|
||||
.await?
|
||||
.ok_or(ExecutorError::TaskNotFound)?; // Reuse TaskNotFound for simplicity
|
||||
|
||||
let child = Command::new("bash")
|
||||
.kill_on_drop(true)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.arg("-c")
|
||||
.arg(&self.script)
|
||||
.current_dir(worktree_path)
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.map_err(ExecutorError::SpawnFailed)?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod amp;
|
||||
pub mod claude;
|
||||
pub mod dev_server;
|
||||
pub mod echo;
|
||||
pub mod setup_script;
|
||||
|
||||
pub use amp::{AmpExecutor, AmpFollowupExecutor};
|
||||
pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor};
|
||||
pub use dev_server::DevServerExecutor;
|
||||
pub use echo::EchoExecutor;
|
||||
pub use setup_script::SetupScriptExecutor;
|
||||
|
||||
@@ -35,6 +35,7 @@ impl Executor for SetupScriptExecutor {
|
||||
.arg("-c")
|
||||
.arg(&self.script)
|
||||
.current_dir(worktree_path)
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.map_err(ExecutorError::SpawnFailed)?;
|
||||
|
||||
|
||||
@@ -83,13 +83,22 @@ async fn serve_file(path: &str) -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_sound_file(axum::extract::Path(filename): axum::extract::Path<String>) -> impl IntoResponse {
|
||||
use tokio::fs;
|
||||
async fn serve_sound_file(
|
||||
axum::extract::Path(filename): axum::extract::Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
|
||||
// Validate filename contains only expected sound files
|
||||
let valid_sounds = ["abstract-sound1.mp3", "abstract-sound2.mp3", "abstract-sound3.mp3",
|
||||
"abstract-sound4.mp3", "cow-mooing.mp3", "phone-vibration.mp3", "rooster.mp3"];
|
||||
let valid_sounds = [
|
||||
"abstract-sound1.mp3",
|
||||
"abstract-sound2.mp3",
|
||||
"abstract-sound3.mp3",
|
||||
"abstract-sound4.mp3",
|
||||
"cow-mooing.mp3",
|
||||
"phone-vibration.mp3",
|
||||
"rooster.mp3",
|
||||
];
|
||||
|
||||
if !valid_sounds.contains(&filename.as_str()) {
|
||||
return Response::builder()
|
||||
@@ -101,19 +110,15 @@ async fn serve_sound_file(axum::extract::Path(filename): axum::extract::Path<Str
|
||||
let sound_path = Path::new("backend/sounds").join(&filename);
|
||||
|
||||
match fs::read(&sound_path).await {
|
||||
Ok(content) => {
|
||||
Response::builder()
|
||||
Ok(content) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, HeaderValue::from_static("audio/mpeg"))
|
||||
.body(Body::from(content))
|
||||
.unwrap()
|
||||
}
|
||||
Err(_) => {
|
||||
Response::builder()
|
||||
.unwrap(),
|
||||
Err(_) => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("Sound file not found"))
|
||||
.unwrap()
|
||||
}
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,24 @@ pub struct UpdateExecutionProcess {
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ExecutionProcessSummary {
|
||||
pub id: Uuid,
|
||||
pub task_attempt_id: Uuid,
|
||||
pub process_type: ExecutionProcessType,
|
||||
pub executor_type: Option<String>, // "echo", "claude", "amp", etc. - only for CodingAgent processes
|
||||
pub status: ExecutionProcessStatus,
|
||||
pub command: String,
|
||||
pub args: Option<String>, // JSON array of arguments
|
||||
pub working_directory: String,
|
||||
pub exit_code: Option<i64>,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ExecutionProcess {
|
||||
/// Find execution process by ID
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
||||
@@ -147,6 +165,36 @@ impl ExecutionProcess {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find execution process summaries for a task attempt (excluding stdio)
|
||||
pub async fn find_summaries_by_task_attempt_id(
|
||||
pool: &SqlitePool,
|
||||
task_attempt_id: Uuid,
|
||||
) -> Result<Vec<ExecutionProcessSummary>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
ExecutionProcessSummary,
|
||||
r#"SELECT
|
||||
id as "id!: Uuid",
|
||||
task_attempt_id as "task_attempt_id!: Uuid",
|
||||
process_type as "process_type!: ExecutionProcessType",
|
||||
executor_type,
|
||||
status as "status!: ExecutionProcessStatus",
|
||||
command,
|
||||
args,
|
||||
working_directory,
|
||||
exit_code,
|
||||
started_at as "started_at!: DateTime<Utc>",
|
||||
completed_at as "completed_at?: DateTime<Utc>",
|
||||
created_at as "created_at!: DateTime<Utc>",
|
||||
updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM execution_processes
|
||||
WHERE task_attempt_id = $1
|
||||
ORDER BY created_at ASC"#,
|
||||
task_attempt_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find running execution processes
|
||||
pub async fn find_running(pool: &SqlitePool) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
@@ -175,6 +223,42 @@ impl ExecutionProcess {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find running dev servers for a specific project
|
||||
pub async fn find_running_dev_servers_by_project(
|
||||
pool: &SqlitePool,
|
||||
project_id: Uuid,
|
||||
) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
ExecutionProcess,
|
||||
r#"SELECT
|
||||
ep.id as "id!: Uuid",
|
||||
ep.task_attempt_id as "task_attempt_id!: Uuid",
|
||||
ep.process_type as "process_type!: ExecutionProcessType",
|
||||
ep.executor_type,
|
||||
ep.status as "status!: ExecutionProcessStatus",
|
||||
ep.command,
|
||||
ep.args,
|
||||
ep.working_directory,
|
||||
ep.stdout,
|
||||
ep.stderr,
|
||||
ep.exit_code,
|
||||
ep.started_at as "started_at!: DateTime<Utc>",
|
||||
ep.completed_at as "completed_at?: DateTime<Utc>",
|
||||
ep.created_at as "created_at!: DateTime<Utc>",
|
||||
ep.updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM execution_processes ep
|
||||
JOIN task_attempts ta ON ep.task_attempt_id = ta.id
|
||||
JOIN tasks t ON ta.task_id = t.id
|
||||
WHERE ep.status = 'running'
|
||||
AND ep.process_type = 'devserver'
|
||||
AND t.project_id = $1
|
||||
ORDER BY ep.created_at ASC"#,
|
||||
project_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a new execution process
|
||||
pub async fn create(
|
||||
pool: &SqlitePool,
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Project {
|
||||
pub name: String,
|
||||
pub git_repo_path: String,
|
||||
pub setup_script: Option<String>,
|
||||
pub dev_script: Option<String>,
|
||||
|
||||
#[ts(type = "Date")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
@@ -25,6 +26,7 @@ pub struct CreateProject {
|
||||
pub git_repo_path: String,
|
||||
pub use_existing_repo: bool,
|
||||
pub setup_script: Option<String>,
|
||||
pub dev_script: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
@@ -33,6 +35,7 @@ pub struct UpdateProject {
|
||||
pub name: Option<String>,
|
||||
pub git_repo_path: Option<String>,
|
||||
pub setup_script: Option<String>,
|
||||
pub dev_script: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, TS)]
|
||||
@@ -55,7 +58,7 @@ impl Project {
|
||||
pub async fn find_all(pool: &SqlitePool) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Project,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects ORDER BY created_at DESC"#
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects ORDER BY created_at DESC"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
@@ -64,7 +67,7 @@ impl Project {
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Project,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE id = $1"#,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE id = $1"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
@@ -77,7 +80,7 @@ impl Project {
|
||||
) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Project,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE git_repo_path = $1"#,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE git_repo_path = $1"#,
|
||||
git_repo_path
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
@@ -91,7 +94,7 @@ impl Project {
|
||||
) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Project,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE git_repo_path = $1 AND id != $2"#,
|
||||
r#"SELECT id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>" FROM projects WHERE git_repo_path = $1 AND id != $2"#,
|
||||
git_repo_path,
|
||||
exclude_id
|
||||
)
|
||||
@@ -106,11 +109,12 @@ impl Project {
|
||||
) -> Result<Self, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Project,
|
||||
r#"INSERT INTO projects (id, name, git_repo_path, setup_script) VALUES ($1, $2, $3, $4) RETURNING id as "id!: Uuid", name, git_repo_path, setup_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
r#"INSERT INTO projects (id, name, git_repo_path, setup_script, dev_script) VALUES ($1, $2, $3, $4, $5) RETURNING id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
project_id,
|
||||
data.name,
|
||||
data.git_repo_path,
|
||||
data.setup_script
|
||||
data.setup_script,
|
||||
data.dev_script
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
@@ -122,14 +126,16 @@ impl Project {
|
||||
name: String,
|
||||
git_repo_path: String,
|
||||
setup_script: Option<String>,
|
||||
dev_script: Option<String>,
|
||||
) -> Result<Self, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Project,
|
||||
r#"UPDATE projects SET name = $2, git_repo_path = $3, setup_script = $4 WHERE id = $1 RETURNING id as "id!: Uuid", name, git_repo_path, setup_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
r#"UPDATE projects SET name = $2, git_repo_path = $3, setup_script = $4, dev_script = $5 WHERE id = $1 RETURNING id as "id!: Uuid", name, git_repo_path, setup_script, dev_script, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
id,
|
||||
name,
|
||||
git_repo_path,
|
||||
setup_script
|
||||
setup_script,
|
||||
dev_script
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
|
||||
@@ -18,6 +18,7 @@ pub enum TaskAttemptError {
|
||||
Git(GitError),
|
||||
TaskNotFound,
|
||||
ProjectNotFound,
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TaskAttemptError {
|
||||
@@ -27,6 +28,7 @@ impl std::fmt::Display for TaskAttemptError {
|
||||
TaskAttemptError::Git(e) => write!(f, "Git error: {}", e),
|
||||
TaskAttemptError::TaskNotFound => write!(f, "Task not found"),
|
||||
TaskAttemptError::ProjectNotFound => write!(f, "Project not found"),
|
||||
TaskAttemptError::ValidationError(e) => write!(f, "Validation error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,6 +488,49 @@ impl TaskAttempt {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start a dev server for this task attempt
|
||||
pub async fn start_dev_server(
|
||||
pool: &SqlitePool,
|
||||
app_state: &crate::app_state::AppState,
|
||||
attempt_id: Uuid,
|
||||
task_id: Uuid,
|
||||
project_id: Uuid,
|
||||
) -> Result<(), TaskAttemptError> {
|
||||
let task_attempt = TaskAttempt::find_by_id(pool, attempt_id)
|
||||
.await?
|
||||
.ok_or(TaskAttemptError::TaskNotFound)?;
|
||||
|
||||
// Get the project to access the dev_script
|
||||
let project = crate::models::project::Project::find_by_id(pool, project_id)
|
||||
.await?
|
||||
.ok_or(TaskAttemptError::TaskNotFound)?;
|
||||
|
||||
let dev_script = project.dev_script.ok_or_else(|| {
|
||||
TaskAttemptError::ValidationError(
|
||||
"No dev script configured for this project".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if dev_script.trim().is_empty() {
|
||||
return Err(TaskAttemptError::ValidationError(
|
||||
"Dev script is empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Self::start_process_execution(
|
||||
pool,
|
||||
app_state,
|
||||
attempt_id,
|
||||
task_id,
|
||||
crate::executor::ExecutorType::DevServer(dev_script),
|
||||
"Starting dev server".to_string(),
|
||||
TaskAttemptStatus::ExecutorRunning, // Dev servers don't create activities, just use generic status
|
||||
crate::models::execution_process::ExecutionProcessType::DevServer,
|
||||
&task_attempt.worktree_path,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start a follow-up execution using the same executor type as the first process
|
||||
pub async fn start_followup_execution(
|
||||
pool: &SqlitePool,
|
||||
@@ -600,9 +645,14 @@ impl TaskAttempt {
|
||||
Self::create_executor_session_record(pool, attempt_id, task_id, process_id).await?;
|
||||
}
|
||||
|
||||
// Create activity record
|
||||
// Create activity record (skip for dev servers as they run in parallel)
|
||||
if !matches!(
|
||||
process_type,
|
||||
crate::models::execution_process::ExecutionProcessType::DevServer
|
||||
) {
|
||||
Self::create_activity_record(pool, process_id, activity_status.clone(), &activity_note)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tracing::info!("Starting {} for task attempt {}", activity_note, attempt_id);
|
||||
|
||||
@@ -646,6 +696,11 @@ impl TaskAttempt {
|
||||
Some(serde_json::to_string(&["-c", "setup_script"]).unwrap()),
|
||||
None, // Setup scripts don't have an executor type
|
||||
),
|
||||
crate::executor::ExecutorType::DevServer(_) => (
|
||||
"bash".to_string(),
|
||||
Some(serde_json::to_string(&["-c", "dev_server"]).unwrap()),
|
||||
None, // Dev servers don't have an executor type
|
||||
),
|
||||
crate::executor::ExecutorType::CodingAgent(config) => {
|
||||
let executor_type_str = match config {
|
||||
crate::executor::ExecutorConfig::Echo => "echo",
|
||||
@@ -748,7 +803,7 @@ impl TaskAttempt {
|
||||
process_id: Uuid,
|
||||
worktree_path: &str,
|
||||
) -> Result<tokio::process::Child, TaskAttemptError> {
|
||||
use crate::executors::SetupScriptExecutor;
|
||||
use crate::executors::{DevServerExecutor, SetupScriptExecutor};
|
||||
|
||||
let result = match executor_type {
|
||||
crate::executor::ExecutorType::SetupScript(script) => {
|
||||
@@ -759,6 +814,14 @@ impl TaskAttempt {
|
||||
.execute_streaming(pool, task_id, attempt_id, process_id, worktree_path)
|
||||
.await
|
||||
}
|
||||
crate::executor::ExecutorType::DevServer(script) => {
|
||||
let executor = DevServerExecutor {
|
||||
script: script.clone(),
|
||||
};
|
||||
executor
|
||||
.execute_streaming(pool, task_id, attempt_id, process_id, worktree_path)
|
||||
.await
|
||||
}
|
||||
crate::executor::ExecutorType::CodingAgent(config) => {
|
||||
let executor = config.create_executor();
|
||||
executor
|
||||
|
||||
@@ -7,7 +7,10 @@ use axum::{
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::models::{config::{Config, EditorConstants, SoundConstants}, ApiResponse};
|
||||
use crate::models::{
|
||||
config::{Config, EditorConstants, SoundConstants},
|
||||
ApiResponse,
|
||||
};
|
||||
use crate::utils;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -205,8 +205,9 @@ pub async fn update_project(
|
||||
.git_repo_path
|
||||
.unwrap_or(existing_project.git_repo_path.clone());
|
||||
let setup_script = payload.setup_script.or(existing_project.setup_script);
|
||||
let dev_script = payload.dev_script.or(existing_project.dev_script);
|
||||
|
||||
match Project::update(&pool, id, name, git_repo_path, setup_script).await {
|
||||
match Project::update(&pool, id, name, git_repo_path, setup_script, dev_script).await {
|
||||
Ok(project) => Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(project),
|
||||
|
||||
@@ -11,7 +11,7 @@ use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{
|
||||
execution_process::ExecutionProcess,
|
||||
execution_process::{ExecutionProcess, ExecutionProcessSummary},
|
||||
task::Task,
|
||||
task_attempt::{
|
||||
BranchStatus, CreateFollowUpAttempt, CreateTaskAttempt, TaskAttempt, TaskAttemptStatus,
|
||||
@@ -431,7 +431,7 @@ pub async fn rebase_task_attempt(
|
||||
pub async fn get_task_attempt_execution_processes(
|
||||
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
|
||||
Extension(pool): Extension<SqlitePool>,
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<ExecutionProcess>>>, StatusCode> {
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<ExecutionProcessSummary>>>, StatusCode> {
|
||||
// Verify task attempt exists and belongs to the correct task
|
||||
match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await {
|
||||
Ok(false) => return Err(StatusCode::NOT_FOUND),
|
||||
@@ -442,7 +442,7 @@ pub async fn get_task_attempt_execution_processes(
|
||||
Ok(true) => {}
|
||||
}
|
||||
|
||||
match ExecutionProcess::find_by_task_attempt_id(&pool, attempt_id).await {
|
||||
match ExecutionProcess::find_summaries_by_task_attempt_id(&pool, attempt_id).await {
|
||||
Ok(processes) => Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(processes),
|
||||
@@ -549,7 +549,11 @@ pub async fn stop_all_execution_processes(
|
||||
tracing::error!("Failed to update execution process status: {}", e);
|
||||
errors.push(format!("Failed to update process {} status", process.id));
|
||||
} else {
|
||||
// Create a new activity record to mark as stopped
|
||||
// Create activity record for stopped processes (skip dev servers)
|
||||
if !matches!(
|
||||
process.process_type,
|
||||
crate::models::execution_process::ExecutionProcessType::DevServer
|
||||
) {
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id: process.id,
|
||||
@@ -576,6 +580,7 @@ pub async fn stop_all_execution_processes(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
// Process was not running, which is fine
|
||||
}
|
||||
@@ -673,7 +678,11 @@ pub async fn stop_execution_process(
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Create a new activity record to mark as stopped
|
||||
// Create activity record for stopped processes (skip dev servers)
|
||||
if !matches!(
|
||||
process.process_type,
|
||||
crate::models::execution_process::ExecutionProcessType::DevServer
|
||||
) {
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id: process_id,
|
||||
@@ -695,6 +704,7 @@ pub async fn stop_execution_process(
|
||||
tracing::error!("Failed to create stopped activity: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
@@ -793,6 +803,86 @@ pub async fn create_followup_attempt(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_dev_server(
|
||||
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
|
||||
Extension(pool): Extension<SqlitePool>,
|
||||
Extension(app_state): Extension<crate::app_state::AppState>,
|
||||
) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
|
||||
// Verify task attempt exists and belongs to the correct task
|
||||
match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await {
|
||||
Ok(false) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to check task attempt existence: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
Ok(true) => {}
|
||||
}
|
||||
|
||||
// Stop any existing dev servers for this project
|
||||
let existing_dev_servers =
|
||||
match ExecutionProcess::find_running_dev_servers_by_project(&pool, project_id).await {
|
||||
Ok(servers) => servers,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to find running dev servers for project {}: {}",
|
||||
project_id,
|
||||
e
|
||||
);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
for dev_server in existing_dev_servers {
|
||||
tracing::info!(
|
||||
"Stopping existing dev server {} for project {}",
|
||||
dev_server.id,
|
||||
project_id
|
||||
);
|
||||
|
||||
// Stop the running process
|
||||
if let Err(e) = app_state.stop_running_execution_by_id(dev_server.id).await {
|
||||
tracing::error!("Failed to stop dev server {}: {}", dev_server.id, e);
|
||||
} else {
|
||||
// Update the execution process status in the database
|
||||
if let Err(e) = ExecutionProcess::update_completion(
|
||||
&pool,
|
||||
dev_server.id,
|
||||
crate::models::execution_process::ExecutionProcessStatus::Killed,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to update dev server {} status: {}",
|
||||
dev_server.id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start dev server execution
|
||||
match TaskAttempt::start_dev_server(&pool, &app_state, attempt_id, task_id, project_id).await {
|
||||
Ok(_) => Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: None,
|
||||
message: Some("Dev server started successfully".to_string()),
|
||||
})),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to start dev server for task attempt {}: {}",
|
||||
attempt_id,
|
||||
e
|
||||
);
|
||||
Ok(ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some(e.to_string()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn task_attempts_router() -> Router {
|
||||
use axum::routing::post;
|
||||
|
||||
@@ -850,4 +940,8 @@ pub fn task_attempts_router() -> Router {
|
||||
"/projects/:project_id/tasks/:task_id/attempts/:attempt_id/follow-up",
|
||||
post(create_followup_attempt),
|
||||
)
|
||||
.route(
|
||||
"/projects/:project_id/tasks/:task_id/attempts/:attempt_id/start-dev-server",
|
||||
post(start_dev_server),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function ProjectForm({
|
||||
const [name, setName] = useState(project?.name || "");
|
||||
const [gitRepoPath, setGitRepoPath] = useState(project?.git_repo_path || "");
|
||||
const [setupScript, setSetupScript] = useState(project?.setup_script ?? "");
|
||||
const [devScript, setDevScript] = useState(project?.dev_script ?? "");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [showFolderPicker, setShowFolderPicker] = useState(false);
|
||||
@@ -47,10 +48,12 @@ export function ProjectForm({
|
||||
setName(project.name || "");
|
||||
setGitRepoPath(project.git_repo_path || "");
|
||||
setSetupScript(project.setup_script ?? "");
|
||||
setDevScript(project.dev_script ?? "");
|
||||
} else {
|
||||
setName("");
|
||||
setGitRepoPath("");
|
||||
setSetupScript("");
|
||||
setDevScript("");
|
||||
}
|
||||
}, [project]);
|
||||
|
||||
@@ -90,6 +93,7 @@ export function ProjectForm({
|
||||
name,
|
||||
git_repo_path: finalGitRepoPath,
|
||||
setup_script: setupScript.trim() || null,
|
||||
dev_script: devScript.trim() || null,
|
||||
};
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${project.id}`,
|
||||
@@ -113,6 +117,7 @@ export function ProjectForm({
|
||||
git_repo_path: finalGitRepoPath,
|
||||
use_existing_repo: repoMode === "existing",
|
||||
setup_script: setupScript.trim() || null,
|
||||
dev_script: devScript.trim() || null,
|
||||
};
|
||||
const response = await makeRequest("/api/projects", {
|
||||
method: "POST",
|
||||
@@ -147,10 +152,12 @@ export function ProjectForm({
|
||||
setName(project.name || "");
|
||||
setGitRepoPath(project.git_repo_path || "");
|
||||
setSetupScript(project.setup_script ?? "");
|
||||
setDevScript(project.dev_script ?? "");
|
||||
} else {
|
||||
setName("");
|
||||
setGitRepoPath("");
|
||||
setSetupScript("");
|
||||
setDevScript("");
|
||||
}
|
||||
setParentPath("");
|
||||
setFolderName("");
|
||||
@@ -316,6 +323,22 @@ export function ProjectForm({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dev-script">Dev Server Script (Optional)</Label>
|
||||
<textarea
|
||||
id="dev-script"
|
||||
value={devScript}
|
||||
onChange={(e) => setDevScript(e.target.value)}
|
||||
placeholder="#!/bin/bash npm run dev # Add dev server start command here..."
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This script can be run from task attempts to start a development server.
|
||||
Use it to quickly start your project's dev server for testing changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
|
||||
@@ -4,13 +4,30 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { FileText, MessageSquare } from "lucide-react";
|
||||
import { ConversationViewer } from "./ConversationViewer";
|
||||
import type { ExecutionProcess } from "shared/types";
|
||||
import type { ExecutionProcess, ExecutionProcessStatus } from "shared/types";
|
||||
|
||||
interface ExecutionOutputViewerProps {
|
||||
executionProcess: ExecutionProcess;
|
||||
executor?: string;
|
||||
}
|
||||
|
||||
const getExecutionProcessStatusDisplay = (
|
||||
status: ExecutionProcessStatus
|
||||
): { label: string; color: string } => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return { label: "Running", color: "bg-blue-500" };
|
||||
case "completed":
|
||||
return { label: "Completed", color: "bg-green-500" };
|
||||
case "failed":
|
||||
return { label: "Failed", color: "bg-red-500" };
|
||||
case "killed":
|
||||
return { label: "Stopped", color: "bg-gray-500" };
|
||||
default:
|
||||
return { label: "Unknown", color: "bg-gray-400" };
|
||||
}
|
||||
};
|
||||
|
||||
export function ExecutionOutputViewer({
|
||||
executionProcess,
|
||||
executor,
|
||||
@@ -93,17 +110,34 @@ export function ExecutionOutputViewer({
|
||||
);
|
||||
}
|
||||
|
||||
const statusDisplay = getExecutionProcessStatusDisplay(executionProcess.status);
|
||||
|
||||
return (
|
||||
<Card className="">
|
||||
<CardContent className="p-3">
|
||||
<div className="space-y-3">
|
||||
{/* Execution process header with status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs capitalize">
|
||||
{executionProcess.process_type.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className={`h-2 w-2 rounded-full ${statusDisplay.color}`} />
|
||||
<span className="text-xs text-muted-foreground">{statusDisplay.label}</span>
|
||||
</div>
|
||||
{executor && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{executor}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View mode toggle for executors with valid JSONL */}
|
||||
{isValidJsonl && hasStdout && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{executor} output
|
||||
</Badge>
|
||||
{jsonlFormat && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{jsonlFormat} format
|
||||
|
||||
@@ -14,12 +14,19 @@ import {
|
||||
StopCircle,
|
||||
Send,
|
||||
AlertCircle,
|
||||
Play,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Chip } from "@/components/ui/chip";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ExecutionOutputViewer } from "./ExecutionOutputViewer";
|
||||
import { EditorSelectionDialog } from "./EditorSelectionDialog";
|
||||
|
||||
@@ -44,11 +51,14 @@ import type {
|
||||
ApiResponse,
|
||||
TaskWithAttemptStatus,
|
||||
ExecutionProcess,
|
||||
ExecutionProcessSummary,
|
||||
EditorType,
|
||||
Project,
|
||||
} from "shared/types";
|
||||
|
||||
interface TaskDetailsPanelProps {
|
||||
task: TaskWithAttemptStatus | null;
|
||||
project: Project | null;
|
||||
projectId: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -125,6 +135,7 @@ const getAttemptStatusDisplay = (
|
||||
|
||||
export function TaskDetailsPanel({
|
||||
task,
|
||||
project,
|
||||
projectId,
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -135,12 +146,16 @@ export function TaskDetailsPanel({
|
||||
const [selectedAttempt, setSelectedAttempt] = useState<TaskAttempt | null>(
|
||||
null
|
||||
);
|
||||
const [attemptActivities, setAttemptActivities] = useState<
|
||||
TaskAttemptActivity[]
|
||||
>([]);
|
||||
const [executionProcesses, setExecutionProcesses] = useState<
|
||||
Record<string, ExecutionProcess>
|
||||
>({});
|
||||
// Combined attempt data state
|
||||
const [attemptData, setAttemptData] = useState<{
|
||||
activities: TaskAttemptActivity[];
|
||||
processes: ExecutionProcessSummary[];
|
||||
runningProcessDetails: Record<string, ExecutionProcess>;
|
||||
}>({
|
||||
activities: [],
|
||||
processes: [],
|
||||
runningProcessDetails: {},
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
const [selectedExecutor, setSelectedExecutor] = useState<string>("claude");
|
||||
@@ -152,26 +167,38 @@ export function TaskDetailsPanel({
|
||||
const [followUpMessage, setFollowUpMessage] = useState("");
|
||||
const [isSendingFollowUp, setIsSendingFollowUp] = useState(false);
|
||||
const [followUpError, setFollowUpError] = useState<string | null>(null);
|
||||
const [isStartingDevServer, setIsStartingDevServer] = useState(false);
|
||||
const [devServerDetails, setDevServerDetails] =
|
||||
useState<ExecutionProcess | null>(null);
|
||||
const [isHoveringDevServer, setIsHoveringDevServer] = useState(false);
|
||||
|
||||
// Auto-scroll state
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { config } = useConfig();
|
||||
|
||||
// Find running dev server in current project (across all task attempts)
|
||||
const runningDevServer = useMemo(() => {
|
||||
return attemptData.processes.find(
|
||||
(process) =>
|
||||
process.process_type === "devserver" && process.status === "running"
|
||||
);
|
||||
}, [attemptData.processes]);
|
||||
|
||||
// Handle ESC key locally to prevent global navigation
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown, true); // Use capture phase
|
||||
return () => document.removeEventListener('keydown', handleKeyDown, true);
|
||||
document.addEventListener("keydown", handleKeyDown, true); // Use capture phase
|
||||
return () => document.removeEventListener("keydown", handleKeyDown, true);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Available executors
|
||||
@@ -182,16 +209,15 @@ export function TaskDetailsPanel({
|
||||
];
|
||||
|
||||
// Check if any execution process is currently running
|
||||
// We need to check the latest activity for each execution process
|
||||
const isAttemptRunning = useMemo(() => {
|
||||
if (!selectedAttempt || attemptActivities.length === 0 || isStopping) {
|
||||
if (!selectedAttempt || attemptData.activities.length === 0 || isStopping) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Group activities by execution_process_id and get the latest one for each
|
||||
const latestActivitiesByProcess = new Map<string, TaskAttemptActivity>();
|
||||
|
||||
attemptActivities.forEach((activity) => {
|
||||
attemptData.activities.forEach((activity) => {
|
||||
const existing = latestActivitiesByProcess.get(
|
||||
activity.execution_process_id
|
||||
);
|
||||
@@ -209,21 +235,31 @@ export function TaskDetailsPanel({
|
||||
activity.status === "setuprunning" ||
|
||||
activity.status === "executorrunning"
|
||||
);
|
||||
}, [selectedAttempt, attemptActivities, isStopping]);
|
||||
}, [selectedAttempt, attemptData.activities, isStopping]);
|
||||
|
||||
// Check if follow-up should be enabled
|
||||
const canSendFollowUp = useMemo(() => {
|
||||
if (!selectedAttempt || attemptActivities.length === 0 || isAttemptRunning || isSendingFollowUp) {
|
||||
if (
|
||||
!selectedAttempt ||
|
||||
attemptData.activities.length === 0 ||
|
||||
isAttemptRunning ||
|
||||
isSendingFollowUp
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Need at least one completed coding agent execution
|
||||
const codingAgentActivities = attemptActivities.filter(
|
||||
const codingAgentActivities = attemptData.activities.filter(
|
||||
(activity) => activity.status === "executorcomplete"
|
||||
);
|
||||
|
||||
return codingAgentActivities.length > 0;
|
||||
}, [selectedAttempt, attemptActivities, isAttemptRunning, isSendingFollowUp]);
|
||||
}, [
|
||||
selectedAttempt,
|
||||
attemptData.activities,
|
||||
isAttemptRunning,
|
||||
isSendingFollowUp,
|
||||
]);
|
||||
|
||||
// Polling for updates when attempt is running
|
||||
useEffect(() => {
|
||||
@@ -231,13 +267,52 @@ export function TaskDetailsPanel({
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (selectedAttempt) {
|
||||
fetchAttemptActivities(selectedAttempt.id, true);
|
||||
fetchAttemptData(selectedAttempt.id, true);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAttemptRunning, task?.id, selectedAttempt?.id]);
|
||||
|
||||
// Fetch dev server details when hovering
|
||||
const fetchDevServerDetails = async () => {
|
||||
if (!runningDevServer || !task || !selectedAttempt) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${runningDevServer.id}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<ExecutionProcess> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setDevServerDetails(result.data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch dev server details:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Poll dev server details while hovering
|
||||
useEffect(() => {
|
||||
if (!isHoveringDevServer || !runningDevServer) {
|
||||
setDevServerDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch immediately
|
||||
fetchDevServerDetails();
|
||||
|
||||
// Then poll every 2 seconds
|
||||
const interval = setInterval(fetchDevServerDetails, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [
|
||||
isHoveringDevServer,
|
||||
runningDevServer?.id,
|
||||
task?.id,
|
||||
selectedAttempt?.id,
|
||||
]);
|
||||
|
||||
// Set default executor from config
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
@@ -254,14 +329,16 @@ export function TaskDetailsPanel({
|
||||
// Auto-scroll to bottom when activities or execution processes change
|
||||
useEffect(() => {
|
||||
if (shouldAutoScroll && scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
scrollContainerRef.current.scrollTop =
|
||||
scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [attemptActivities, executionProcesses, shouldAutoScroll]);
|
||||
}, [attemptData.activities, attemptData.processes, shouldAutoScroll]);
|
||||
|
||||
// Handle scroll events to detect manual scrolling
|
||||
const handleScroll = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
||||
const { scrollTop, scrollHeight, clientHeight } =
|
||||
scrollContainerRef.current;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5; // 5px tolerance
|
||||
|
||||
if (isAtBottom && !shouldAutoScroll) {
|
||||
@@ -294,12 +371,15 @@ export function TaskDetailsPanel({
|
||||
: latest
|
||||
);
|
||||
setSelectedAttempt(latestAttempt);
|
||||
fetchAttemptActivities(latestAttempt.id);
|
||||
fetchAttemptData(latestAttempt.id);
|
||||
} else {
|
||||
// Clear state when no attempts exist
|
||||
setSelectedAttempt(null);
|
||||
setAttemptActivities([]);
|
||||
setExecutionProcesses({});
|
||||
setAttemptData({
|
||||
activities: [],
|
||||
processes: [],
|
||||
runningProcessDetails: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,59 +390,74 @@ export function TaskDetailsPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAttemptActivities = async (
|
||||
const fetchAttemptData = async (
|
||||
attemptId: string,
|
||||
_isBackgroundUpdate = false
|
||||
) => {
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
const [activitiesResponse, processesResponse] = await Promise.all([
|
||||
makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities`
|
||||
);
|
||||
),
|
||||
makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/execution-processes`
|
||||
),
|
||||
]);
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<TaskAttemptActivity[]> =
|
||||
await response.json();
|
||||
if (result.success && result.data) {
|
||||
setAttemptActivities(result.data);
|
||||
if (activitiesResponse.ok && processesResponse.ok) {
|
||||
const activitiesResult: ApiResponse<TaskAttemptActivity[]> =
|
||||
await activitiesResponse.json();
|
||||
const processesResult: ApiResponse<ExecutionProcessSummary[]> =
|
||||
await processesResponse.json();
|
||||
|
||||
// Fetch execution processes for running activities
|
||||
const runningActivities = result.data.filter(
|
||||
if (
|
||||
activitiesResult.success &&
|
||||
processesResult.success &&
|
||||
activitiesResult.data &&
|
||||
processesResult.data
|
||||
) {
|
||||
// Find running activities that need detailed execution info
|
||||
const runningActivities = activitiesResult.data.filter(
|
||||
(activity) =>
|
||||
activity.status === "setuprunning" ||
|
||||
activity.status === "executorrunning"
|
||||
);
|
||||
|
||||
// Fetch detailed execution info for running processes
|
||||
const runningProcessDetails: Record<string, ExecutionProcess> = {};
|
||||
for (const activity of runningActivities) {
|
||||
fetchExecutionProcess(activity.execution_process_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch attempt activities:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchExecutionProcess = async (processId: string) => {
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${processId}`
|
||||
const detailResponse = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${activity.execution_process_id}`
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<ExecutionProcess> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setExecutionProcesses((prev) => ({
|
||||
...prev,
|
||||
[processId]: result.data!,
|
||||
}));
|
||||
if (detailResponse.ok) {
|
||||
const detailResult: ApiResponse<ExecutionProcess> =
|
||||
await detailResponse.json();
|
||||
if (detailResult.success && detailResult.data) {
|
||||
runningProcessDetails[activity.execution_process_id] =
|
||||
detailResult.data;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch execution process:", err);
|
||||
console.error(
|
||||
`Failed to fetch execution process ${activity.execution_process_id}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update all attempt data at once
|
||||
setAttemptData({
|
||||
activities: activitiesResult.data,
|
||||
processes: processesResult.data,
|
||||
runningProcessDetails,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch attempt data:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -370,7 +465,7 @@ export function TaskDetailsPanel({
|
||||
const attempt = taskAttempts.find((a) => a.id === attemptId);
|
||||
if (attempt) {
|
||||
setSelectedAttempt(attempt);
|
||||
fetchAttemptActivities(attempt.id);
|
||||
fetchAttemptData(attempt.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -401,6 +496,70 @@ export function TaskDetailsPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const startDevServer = async () => {
|
||||
if (!task || !selectedAttempt || !project?.dev_script) return;
|
||||
|
||||
setIsStartingDevServer(true);
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/start-dev-server`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to start dev server");
|
||||
}
|
||||
|
||||
const data: ApiResponse<null> = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || "Failed to start dev server");
|
||||
}
|
||||
|
||||
// Refresh activities to show the new dev server process
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} catch (err) {
|
||||
console.error("Failed to start dev server:", err);
|
||||
} finally {
|
||||
setIsStartingDevServer(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopDevServer = async () => {
|
||||
if (!task || !selectedAttempt || !runningDevServer) return;
|
||||
|
||||
setIsStartingDevServer(true);
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/execution-processes/${runningDevServer.id}/stop`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to stop dev server");
|
||||
}
|
||||
|
||||
// Refresh activities to show the stopped dev server
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} catch (err) {
|
||||
console.error("Failed to stop dev server:", err);
|
||||
} finally {
|
||||
setIsStartingDevServer(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewAttempt = async (executor?: string) => {
|
||||
if (!task) return;
|
||||
|
||||
@@ -443,13 +602,11 @@ export function TaskDetailsPanel({
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
// Clear cached execution processes since they should be stopped
|
||||
setExecutionProcesses({});
|
||||
// Refresh activities to show updated status
|
||||
await fetchAttemptActivities(selectedAttempt.id);
|
||||
await fetchAttemptData(selectedAttempt.id);
|
||||
// Wait a bit for the backend to finish updating
|
||||
setTimeout(() => {
|
||||
fetchAttemptActivities(selectedAttempt.id);
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
}, 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -494,13 +651,21 @@ export function TaskDetailsPanel({
|
||||
// Clear the message
|
||||
setFollowUpMessage("");
|
||||
// Refresh activities to show the new follow-up execution
|
||||
fetchAttemptActivities(selectedAttempt.id);
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
setFollowUpError(`Failed to start follow-up execution: ${errorText || response.statusText}`);
|
||||
setFollowUpError(
|
||||
`Failed to start follow-up execution: ${
|
||||
errorText || response.statusText
|
||||
}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setFollowUpError(`Failed to send follow-up: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
setFollowUpError(
|
||||
`Failed to send follow-up: ${
|
||||
err instanceof Error ? err.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setIsSendingFollowUp(false);
|
||||
}
|
||||
@@ -618,9 +783,6 @@ export function TaskDetailsPanel({
|
||||
selectedAttempt.created_at
|
||||
).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
Worktree: {selectedAttempt.worktree_path}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
@@ -721,6 +883,83 @@ export function TaskDetailsPanel({
|
||||
{isStopping ? "Stopping..." : "Stop"}
|
||||
</Button>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={
|
||||
!project?.dev_script ? "cursor-not-allowed" : ""
|
||||
}
|
||||
onMouseEnter={() => setIsHoveringDevServer(true)}
|
||||
onMouseLeave={() => setIsHoveringDevServer(false)}
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
runningDevServer ? "destructive" : "outline"
|
||||
}
|
||||
size="sm"
|
||||
onClick={
|
||||
runningDevServer
|
||||
? stopDevServer
|
||||
: startDevServer
|
||||
}
|
||||
disabled={
|
||||
isStartingDevServer || !project?.dev_script
|
||||
}
|
||||
>
|
||||
{runningDevServer ? (
|
||||
<StopCircle className="h-4 w-4 mr-1" />
|
||||
) : (
|
||||
<Play className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
{isStartingDevServer
|
||||
? runningDevServer
|
||||
? "Stopping..."
|
||||
: "Starting..."
|
||||
: runningDevServer
|
||||
? "Stop Dev Server"
|
||||
: "Start Dev Server"}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className={runningDevServer ? "max-w-2xl p-4" : ""}
|
||||
side="top"
|
||||
align="center"
|
||||
avoidCollisions={true}
|
||||
>
|
||||
{!project?.dev_script ? (
|
||||
<p>
|
||||
Configure a dev server command in project
|
||||
settings
|
||||
</p>
|
||||
) : runningDevServer && devServerDetails ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">
|
||||
Dev Server Logs (Last 10 lines):
|
||||
</p>
|
||||
<pre className="text-xs bg-muted p-2 rounded max-h-64 overflow-y-auto whitespace-pre-wrap">
|
||||
{(() => {
|
||||
const stdout =
|
||||
devServerDetails.stdout || "";
|
||||
const stderr =
|
||||
devServerDetails.stderr || "";
|
||||
const allOutput =
|
||||
stdout + (stderr ? "\n" + stderr : "");
|
||||
const lines = allOutput
|
||||
.split("\n")
|
||||
.filter((line) => line.trim());
|
||||
const lastLines = lines.slice(-10);
|
||||
return lastLines.length > 0
|
||||
? lastLines.join("\n")
|
||||
: "No output yet...";
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -761,7 +1000,7 @@ export function TaskDetailsPanel({
|
||||
<Label className="text-sm font-medium mb-3 block">
|
||||
Activity History
|
||||
</Label>
|
||||
{attemptActivities.length === 0 ? (
|
||||
{attemptData.activities.length === 0 ? (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
No activities found
|
||||
</div>
|
||||
@@ -790,7 +1029,7 @@ export function TaskDetailsPanel({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{attemptActivities.slice().map((activity) => (
|
||||
{attemptData.activities.slice().map((activity) => (
|
||||
<div key={activity.id}>
|
||||
{/* Compact activity message */}
|
||||
<div className="flex items-center gap-3 my-4 rounded-md">
|
||||
@@ -825,7 +1064,7 @@ export function TaskDetailsPanel({
|
||||
{/* Show stdio output for running processes */}
|
||||
{(activity.status === "setuprunning" ||
|
||||
activity.status === "executorrunning") &&
|
||||
executionProcesses[
|
||||
attemptData.runningProcessDetails[
|
||||
activity.execution_process_id
|
||||
] && (
|
||||
<div className="mt-2">
|
||||
@@ -840,7 +1079,7 @@ export function TaskDetailsPanel({
|
||||
>
|
||||
<ExecutionOutputViewer
|
||||
executionProcess={
|
||||
executionProcesses[
|
||||
attemptData.runningProcessDetails[
|
||||
activity.execution_process_id
|
||||
]
|
||||
}
|
||||
@@ -908,9 +1147,13 @@ export function TaskDetailsPanel({
|
||||
if (followUpError) setFollowUpError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (canSendFollowUp && followUpMessage.trim() && !isSendingFollowUp) {
|
||||
if (
|
||||
canSendFollowUp &&
|
||||
followUpMessage.trim() &&
|
||||
!isSendingFollowUp
|
||||
) {
|
||||
handleSendFollowUp();
|
||||
}
|
||||
}
|
||||
@@ -920,7 +1163,11 @@ export function TaskDetailsPanel({
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSendFollowUp}
|
||||
disabled={!canSendFollowUp || !followUpMessage.trim() || isSendingFollowUp}
|
||||
disabled={
|
||||
!canSendFollowUp ||
|
||||
!followUpMessage.trim() ||
|
||||
isSendingFollowUp
|
||||
}
|
||||
className="self-end"
|
||||
>
|
||||
{isSendingFollowUp ? (
|
||||
|
||||
@@ -415,6 +415,7 @@ export function ProjectTasks() {
|
||||
{isPanelOpen && (
|
||||
<TaskDetailsPanel
|
||||
task={selectedTask}
|
||||
project={project}
|
||||
projectId={projectId!}
|
||||
isOpen={isPanelOpen}
|
||||
onClose={handleClosePanel}
|
||||
|
||||
@@ -24,11 +24,11 @@ export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type
|
||||
|
||||
export type ExecutorConstants = { executor_types: Array<ExecutorConfig>, executor_labels: Array<string>, };
|
||||
|
||||
export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, };
|
||||
export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, dev_script: string | null, };
|
||||
|
||||
export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, created_at: Date, updated_at: Date, };
|
||||
export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, created_at: Date, updated_at: Date, };
|
||||
|
||||
export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, };
|
||||
export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, };
|
||||
|
||||
export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };
|
||||
|
||||
@@ -74,6 +74,8 @@ export type BranchStatus = { is_behind: boolean, commits_behind: number, commits
|
||||
|
||||
export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, stdout: string | null, stderr: string | null, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, };
|
||||
|
||||
export type ExecutionProcessSummary = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, };
|
||||
|
||||
export type ExecutionProcessStatus = "running" | "completed" | "failed" | "killed";
|
||||
|
||||
export type ExecutionProcessType = "setupscript" | "codingagent" | "devserver";
|
||||
|
||||
Reference in New Issue
Block a user