Squashed commit of the following:

commit 93babc04486e64ae55c106478d5c04a9ec891c1f
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 01:05:31 2025 +0100

    UX

commit 91c93187290e4e0882018c392dd744eba7cd2193
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 01:03:39 2025 +0100

    Update TaskDetailsPanel.tsx

    Follow up UI

commit b66cfbfa727eb7d69b2250102712d6169a3af3b1
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 00:58:21 2025 +0100

    Tweaks

commit aa2235c56413ffe88c4ec1bf7950012c019f9455
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 00:34:02 2025 +0100

    Add follow up endpoint

commit 1b536e33c956e39881d5ddfd169d229cfba99c20
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 00:12:55 2025 +0100

    Track executor type

commit 1c5d208f62fce2ed36e04384e139884e85dcb295
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Mon Jun 23 16:56:58 2025 +0100

    Add executor_session

commit 8e305953afb71d096079587df94cf5e63c4c6a04
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Mon Jun 23 16:49:07 2025 +0100

    Fix type issue

commit bc2dcf4fd4926ca2a42d71cd429de66fd1215208
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Mon Jun 23 16:03:27 2025 +0100

    Refactor
This commit is contained in:
Louis Knight-Webb
2025-06-24 01:05:55 +01:00
parent 9149db36a4
commit 1baa25089e
26 changed files with 1434 additions and 225 deletions

View File

@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT \n id as \"id!: Uuid\", \n task_attempt_id as \"task_attempt_id!: Uuid\", \n execution_process_id as \"execution_process_id!: Uuid\", \n session_id, \n prompt,\n created_at as \"created_at!: DateTime<Utc>\", \n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM executor_sessions \n WHERE execution_process_id = $1",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "task_attempt_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "execution_process_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "session_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "prompt",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "0468aa522ed7fd2675bcf278f6be38ce16752cb73adf0fafc5b497a88f32f531"
}

View File

@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT \n id as \"id!: Uuid\", \n task_attempt_id as \"task_attempt_id!: Uuid\", \n execution_process_id as \"execution_process_id!: Uuid\", \n session_id, \n prompt,\n created_at as \"created_at!: DateTime<Utc>\", \n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM executor_sessions \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": "execution_process_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "session_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "prompt",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "06ca282915d0db9125769b1bca92f7a5bd7f81ad8faf0f9fcbb5f1c2d35dd67f"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM executor_sessions WHERE task_attempt_id = $1",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "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 status as \"status!: ExecutionProcessStatus\",\n command, \n args, \n working_directory, \n stdout, \n stderr, \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 status = 'running' \n ORDER BY created_at ASC", "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 stdout, \n stderr, \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 status = 'running' \n ORDER BY created_at ASC",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -19,59 +19,64 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "status!: ExecutionProcessStatus", "name": "executor_type",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "command", "name": "status!: ExecutionProcessStatus",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "args", "name": "command",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "working_directory", "name": "args",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stdout", "name": "working_directory",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stderr", "name": "stdout",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "exit_code", "name": "stderr",
"ordinal": 9, "ordinal": 9,
"type_info": "Text"
},
{
"name": "exit_code",
"ordinal": 10,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "started_at!: DateTime<Utc>", "name": "started_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "completed_at?: DateTime<Utc>",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "created_at!: DateTime<Utc>", "name": "completed_at?: DateTime<Utc>",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "updated_at!: DateTime<Utc>", "name": "created_at!: DateTime<Utc>",
"ordinal": 13, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 14,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -81,6 +86,7 @@
true, true,
false, false,
false, false,
true,
false, false,
false, false,
true, true,
@@ -94,5 +100,5 @@
false false
] ]
}, },
"hash": "d25396768d88ecab6e13ad9fca8e8c46e92ff17474ebd24657384e130c49afa8" "hash": "1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca"
} }

View File

@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT \n id as \"id!: Uuid\", \n task_attempt_id as \"task_attempt_id!: Uuid\", \n execution_process_id as \"execution_process_id!: Uuid\", \n session_id, \n prompt,\n created_at as \"created_at!: DateTime<Utc>\", \n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM executor_sessions \n WHERE id = $1",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "task_attempt_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "execution_process_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "session_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "prompt",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "3b65c0f6215229f3c8d487c204bca5a1a8e327d9b469b47d833befa95377dfab"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "INSERT INTO execution_processes (\n id, task_attempt_id, process_type, status, command, args, \n working_directory, stdout, stderr, exit_code, started_at, \n completed_at, created_at, updated_at\n ) \n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) \n RETURNING \n id as \"id!: Uuid\", \n task_attempt_id as \"task_attempt_id!: Uuid\", \n process_type as \"process_type!: ExecutionProcessType\",\n status as \"status!: ExecutionProcessStatus\",\n command, \n args, \n working_directory, \n stdout, \n stderr, \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>\"", "query": "INSERT INTO execution_processes (\n id, task_attempt_id, process_type, executor_type, status, command, args, \n working_directory, stdout, stderr, exit_code, started_at, \n completed_at, created_at, updated_at\n ) \n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) \n RETURNING \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 stdout, \n stderr, \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>\"",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -19,68 +19,74 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "status!: ExecutionProcessStatus", "name": "executor_type",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "command", "name": "status!: ExecutionProcessStatus",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "args", "name": "command",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "working_directory", "name": "args",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stdout", "name": "working_directory",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stderr", "name": "stdout",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "exit_code", "name": "stderr",
"ordinal": 9, "ordinal": 9,
"type_info": "Text"
},
{
"name": "exit_code",
"ordinal": 10,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "started_at!: DateTime<Utc>", "name": "started_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "completed_at?: DateTime<Utc>",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "created_at!: DateTime<Utc>", "name": "completed_at?: DateTime<Utc>",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "updated_at!: DateTime<Utc>", "name": "created_at!: DateTime<Utc>",
"ordinal": 13, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 14,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
"Right": 14 "Right": 15
}, },
"nullable": [ "nullable": [
true, true,
false, false,
false, false,
true,
false, false,
false, false,
true, true,
@@ -94,5 +100,5 @@
false false
] ]
}, },
"hash": "b50af42f635dec3167508f3c4f81d03911102a603ac94b22a431a513d36471b0" "hash": "5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b"
} }

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE executor_sessions \n SET session_id = $1, updated_at = datetime('now') \n WHERE execution_process_id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "77857828471571c0584bfed1641c4cfe15dba6dcacbb9e9bc10228a8af6da619"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "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 status as \"status!: ExecutionProcessStatus\",\n command, \n args, \n working_directory, \n stdout, \n stderr, \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 id = $1", "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 stdout, \n stderr, \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": { "describe": {
"columns": [ "columns": [
{ {
@@ -19,59 +19,64 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "status!: ExecutionProcessStatus", "name": "executor_type",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "command", "name": "status!: ExecutionProcessStatus",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "args", "name": "command",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "working_directory", "name": "args",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stdout", "name": "working_directory",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stderr", "name": "stdout",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "exit_code", "name": "stderr",
"ordinal": 9, "ordinal": 9,
"type_info": "Text"
},
{
"name": "exit_code",
"ordinal": 10,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "started_at!: DateTime<Utc>", "name": "started_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "completed_at?: DateTime<Utc>",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "created_at!: DateTime<Utc>", "name": "completed_at?: DateTime<Utc>",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "updated_at!: DateTime<Utc>", "name": "created_at!: DateTime<Utc>",
"ordinal": 13, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 14,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -81,6 +86,7 @@
true, true,
false, false,
false, false,
true,
false, false,
false, false,
true, true,
@@ -94,5 +100,5 @@
false false
] ]
}, },
"hash": "1ada5613889792cd6098da71ce2ba1ecdee7e5dc2ff8196872368fff0caa48d8" "hash": "9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "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 status as \"status!: ExecutionProcessStatus\",\n command, \n args, \n working_directory, \n stdout, \n stderr, \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", "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 stdout, \n stderr, \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 id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -19,59 +19,64 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "status!: ExecutionProcessStatus", "name": "executor_type",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "command", "name": "status!: ExecutionProcessStatus",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "args", "name": "command",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "working_directory", "name": "args",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stdout", "name": "working_directory",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "stderr", "name": "stdout",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "exit_code", "name": "stderr",
"ordinal": 9, "ordinal": 9,
"type_info": "Text"
},
{
"name": "exit_code",
"ordinal": 10,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "started_at!: DateTime<Utc>", "name": "started_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "completed_at?: DateTime<Utc>",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "created_at!: DateTime<Utc>", "name": "completed_at?: DateTime<Utc>",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "updated_at!: DateTime<Utc>", "name": "created_at!: DateTime<Utc>",
"ordinal": 13, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 14,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -81,6 +86,7 @@
true, true,
false, false,
false, false,
true,
false, false,
false, false,
true, true,
@@ -94,5 +100,5 @@
false false
] ]
}, },
"hash": "14ad9267623a3aa678a943db6d0b14581a6d353da673c9e4156e0bbeac0b3346" "hash": "9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7"
} }

View File

@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "INSERT INTO executor_sessions (\n id, task_attempt_id, execution_process_id, session_id, prompt, \n created_at, updated_at\n ) \n VALUES ($1, $2, $3, $4, $5, $6, $7) \n RETURNING \n id as \"id!: Uuid\", \n task_attempt_id as \"task_attempt_id!: Uuid\", \n execution_process_id as \"execution_process_id!: Uuid\", \n session_id, \n prompt,\n created_at as \"created_at!: DateTime<Utc>\", \n updated_at as \"updated_at!: DateTime<Utc>\"",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "task_attempt_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "execution_process_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "session_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "prompt",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 7
},
"nullable": [
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "a528a9926fab1c819a5a1fa1cde87ea9d354da0873af22e888d0bf8e0c7f306a"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE executor_sessions \n SET prompt = $1, updated_at = datetime('now') \n WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846"
}

View File

@@ -0,0 +1,17 @@
PRAGMA foreign_keys = ON;
CREATE TABLE executor_sessions (
id BLOB PRIMARY KEY,
task_attempt_id BLOB NOT NULL,
execution_process_id BLOB NOT NULL,
session_id TEXT, -- External session ID from Claude/Amp
prompt TEXT, -- The prompt sent to the executor
created_at TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),
FOREIGN KEY (task_attempt_id) REFERENCES task_attempts(id) ON DELETE CASCADE,
FOREIGN KEY (execution_process_id) REFERENCES execution_processes(id) ON DELETE CASCADE
);
CREATE INDEX idx_executor_sessions_task_attempt_id ON executor_sessions(task_attempt_id);
CREATE INDEX idx_executor_sessions_execution_process_id ON executor_sessions(execution_process_id);
CREATE INDEX idx_executor_sessions_session_id ON executor_sessions(session_id);

View File

@@ -0,0 +1,4 @@
PRAGMA foreign_keys = ON;
-- Add executor_type column to execution_processes table
ALTER TABLE execution_processes ADD COLUMN executor_type TEXT;

View File

@@ -1,6 +1,40 @@
use std::{env, fs, path::Path}; use std::{env, fs, path::Path};
use ts_rs::TS; // in [build-dependencies] use ts_rs::TS; // in [build-dependencies]
fn generate_constants() -> String {
r#"// Generated constants
export const EXECUTOR_TYPES: string[] = [
"echo",
"claude",
"amp"
];
export const EDITOR_TYPES: EditorType[] = [
"vscode",
"cursor",
"windsurf",
"intellij",
"zed",
"custom"
];
export const EXECUTOR_LABELS: Record<string, string> = {
"echo": "Echo (Test Mode)",
"claude": "Claude",
"amp": "Amp"
};
export const EDITOR_LABELS: Record<string, string> = {
"vscode": "VS Code",
"cursor": "Cursor",
"windsurf": "Windsurf",
"intellij": "IntelliJ IDEA",
"zed": "Zed",
"custom": "Custom"
};"#
.to_string()
}
fn main() { fn main() {
// 1. Make sure ../shared exists // 1. Make sure ../shared exists
let shared_path = Path::new("../shared"); let shared_path = Path::new("../shared");
@@ -18,7 +52,9 @@ fn main() {
vibe_kanban::models::config::ThemeMode::decl(), vibe_kanban::models::config::ThemeMode::decl(),
vibe_kanban::models::config::EditorConfig::decl(), vibe_kanban::models::config::EditorConfig::decl(),
vibe_kanban::models::config::EditorType::decl(), vibe_kanban::models::config::EditorType::decl(),
vibe_kanban::models::config::EditorConstants::decl(),
vibe_kanban::executor::ExecutorConfig::decl(), vibe_kanban::executor::ExecutorConfig::decl(),
vibe_kanban::executor::ExecutorConstants::decl(),
vibe_kanban::models::project::CreateProject::decl(), vibe_kanban::models::project::CreateProject::decl(),
vibe_kanban::models::project::Project::decl(), vibe_kanban::models::project::Project::decl(),
vibe_kanban::models::project::UpdateProject::decl(), vibe_kanban::models::project::UpdateProject::decl(),
@@ -34,6 +70,7 @@ fn main() {
vibe_kanban::models::task_attempt::TaskAttempt::decl(), vibe_kanban::models::task_attempt::TaskAttempt::decl(),
vibe_kanban::models::task_attempt::CreateTaskAttempt::decl(), vibe_kanban::models::task_attempt::CreateTaskAttempt::decl(),
vibe_kanban::models::task_attempt::UpdateTaskAttempt::decl(), vibe_kanban::models::task_attempt::UpdateTaskAttempt::decl(),
vibe_kanban::models::task_attempt::CreateFollowUpAttempt::decl(),
vibe_kanban::models::task_attempt_activity::TaskAttemptActivity::decl(), vibe_kanban::models::task_attempt_activity::TaskAttemptActivity::decl(),
vibe_kanban::models::task_attempt_activity::CreateTaskAttemptActivity::decl(), vibe_kanban::models::task_attempt_activity::CreateTaskAttemptActivity::decl(),
vibe_kanban::routes::filesystem::DirectoryEntry::decl(), vibe_kanban::routes::filesystem::DirectoryEntry::decl(),
@@ -47,6 +84,9 @@ fn main() {
vibe_kanban::models::execution_process::ExecutionProcessType::decl(), vibe_kanban::models::execution_process::ExecutionProcessType::decl(),
vibe_kanban::models::execution_process::CreateExecutionProcess::decl(), vibe_kanban::models::execution_process::CreateExecutionProcess::decl(),
vibe_kanban::models::execution_process::UpdateExecutionProcess::decl(), vibe_kanban::models::execution_process::UpdateExecutionProcess::decl(),
vibe_kanban::models::executor_session::ExecutorSession::decl(),
vibe_kanban::models::executor_session::CreateExecutorSession::decl(),
vibe_kanban::models::executor_session::UpdateExecutorSession::decl(),
]; ];
// 4. Friendly banner // 4. Friendly banner
@@ -69,9 +109,15 @@ fn main() {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n\n"); .join("\n\n");
// 6. Write the consolidated types.ts // 6. Add constants
fs::write(shared_path.join("types.ts"), format!("{HEADER}{body}")) let constants = generate_constants();
.expect("unable to write types.ts");
// 7. Write the consolidated types.ts
fs::write(
shared_path.join("types.ts"),
format!("{HEADER}{body}\n\n{constants}"),
)
.expect("unable to write types.ts");
println!("✅ TypeScript types generated in ../shared/"); println!("✅ TypeScript types generated in ../shared/");
} }

View File

@@ -92,6 +92,11 @@ pub trait Executor: Send + Sync {
pub enum ExecutorType { pub enum ExecutorType {
SetupScript(String), SetupScript(String),
CodingAgent(ExecutorConfig), CodingAgent(ExecutorConfig),
FollowUpCodingAgent {
config: ExecutorConfig,
session_id: Option<String>,
prompt: String,
},
} }
/// Configuration for different executor types /// Configuration for different executor types
@@ -107,6 +112,31 @@ pub enum ExecutorConfig {
// Docker { image: String, command: String }, // Docker { image: String, command: String },
} }
// Constants for frontend
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ExecutorConstants {
pub executor_types: Vec<ExecutorConfig>,
pub executor_labels: Vec<String>,
}
impl ExecutorConstants {
pub fn new() -> Self {
Self {
executor_types: vec![
ExecutorConfig::Echo,
ExecutorConfig::Claude,
ExecutorConfig::Amp,
],
executor_labels: vec![
"Echo (Test Mode)".to_string(),
"Claude".to_string(),
"Amp".to_string(),
],
}
}
}
impl ExecutorConfig { impl ExecutorConfig {
pub fn create_executor(&self) -> Box<dyn Executor> { pub fn create_executor(&self) -> Box<dyn Executor> {
match self { match self {
@@ -126,17 +156,45 @@ pub async fn stream_output_to_db(
is_stdout: bool, is_stdout: bool,
) { ) {
use crate::models::execution_process::ExecutionProcess; use crate::models::execution_process::ExecutionProcess;
use crate::models::executor_session::ExecutorSession;
let mut reader = BufReader::new(output); let mut reader = BufReader::new(output);
let mut line = String::new(); let mut line = String::new();
let mut accumulated_output = String::new(); let mut accumulated_output = String::new();
let mut update_counter = 0; let mut update_counter = 0;
let mut session_id_parsed = false;
loop { loop {
line.clear(); line.clear();
match reader.read_line(&mut line).await { match reader.read_line(&mut line).await {
Ok(0) => break, // EOF Ok(0) => break, // EOF
Ok(_) => { Ok(_) => {
// Parse session ID from the first JSONL line (stdout only)
if is_stdout && !session_id_parsed {
if let Some(external_session_id) = parse_session_id_from_line(&line) {
if let Err(e) = ExecutorSession::update_session_id(
&pool,
execution_process_id,
&external_session_id,
)
.await
{
tracing::error!(
"Failed to update session ID for execution process {}: {}",
execution_process_id,
e
);
} else {
tracing::info!(
"Updated session ID {} for execution process {}",
external_session_id,
execution_process_id
);
}
session_id_parsed = true;
}
}
accumulated_output.push_str(&line); accumulated_output.push_str(&line);
update_counter += 1; update_counter += 1;
@@ -208,3 +266,71 @@ pub async fn stream_output_to_db(
} }
} }
} }
/// Parse session_id from Claude or thread_id from Amp from the first JSONL line
fn parse_session_id_from_line(line: &str) -> Option<String> {
use serde_json::Value;
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
// Try to parse as JSON
if let Ok(json) = serde_json::from_str::<Value>(trimmed) {
// Check for Claude session_id
if let Some(session_id) = json.get("session_id").and_then(|v| v.as_str()) {
return Some(session_id.to_string());
}
// Check for Amp threadID
if let Some(thread_id) = json.get("threadID").and_then(|v| v.as_str()) {
return Some(thread_id.to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_claude_session_id() {
let claude_line = r#"{"type":"system","subtype":"init","cwd":"/private/tmp/mission-control-worktree-3abb979d-2e0e-4404-a276-c16d98a97dd5","session_id":"cc0889a2-0c59-43cc-926b-739a983888a2","tools":["Task","Bash","Glob","Grep","LS","exit_plan_mode","Read","Edit","MultiEdit","Write","NotebookRead","NotebookEdit","WebFetch","TodoRead","TodoWrite","WebSearch"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"bypassPermissions","apiKeySource":"/login managed key"}"#;
assert_eq!(
parse_session_id_from_line(claude_line),
Some("cc0889a2-0c59-43cc-926b-739a983888a2".to_string())
);
}
#[test]
fn test_parse_amp_thread_id() {
let amp_line = r#"{"type":"initial","threadID":"T-286f908a-2cd8-40cc-9490-da689b2f1560"}"#;
assert_eq!(
parse_session_id_from_line(amp_line),
Some("T-286f908a-2cd8-40cc-9490-da689b2f1560".to_string())
);
}
#[test]
fn test_parse_invalid_json() {
let invalid_line = "not json at all";
assert_eq!(parse_session_id_from_line(invalid_line), None);
}
#[test]
fn test_parse_json_without_ids() {
let other_json = r#"{"type":"other","message":"hello"}"#;
assert_eq!(parse_session_id_from_line(other_json), None);
}
#[test]
fn test_parse_empty_line() {
assert_eq!(parse_session_id_from_line(""), None);
assert_eq!(parse_session_id_from_line(" "), None);
}
}

View File

@@ -5,9 +5,15 @@ use uuid::Uuid;
use crate::executor::{Executor, ExecutorError}; use crate::executor::{Executor, ExecutorError};
use crate::models::task::Task; use crate::models::task::Task;
/// An executor that uses Claude CLI to process tasks /// An executor that uses Amp to process tasks
pub struct AmpExecutor; pub struct AmpExecutor;
/// An executor that continues an Amp thread
pub struct AmpFollowupExecutor {
pub thread_id: String,
pub prompt: String,
}
#[async_trait] #[async_trait]
impl Executor for AmpExecutor { impl Executor for AmpExecutor {
async fn spawn( async fn spawn(
@@ -53,3 +59,39 @@ impl Executor for AmpExecutor {
Ok(child) Ok(child)
} }
} }
#[async_trait]
impl Executor for AmpFollowupExecutor {
async fn spawn(
&self,
pool: &sqlx::SqlitePool,
task_id: Uuid,
worktree_path: &str,
) -> Result<Child, ExecutorError> {
use std::process::Stdio;
use tokio::{io::AsyncWriteExt, process::Command};
let mut child = Command::new("npx")
.kill_on_drop(true)
.stdin(Stdio::piped()) // <-- open a pipe
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.current_dir(worktree_path)
.arg("@sourcegraph/amp")
.arg("threads")
.arg("continue")
.arg(&self.thread_id)
.arg("--format=jsonl")
.process_group(0) // Create new process group so we can kill entire tree
.spawn()
.map_err(ExecutorError::SpawnFailed)?;
// feed the prompt in, then close the pipe so `amp` sees EOF
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(self.prompt.as_bytes()).await.unwrap();
stdin.shutdown().await.unwrap(); // or `drop(stdin);`
}
Ok(child)
}
}

View File

@@ -8,6 +8,12 @@ use crate::models::task::Task;
/// An executor that uses Claude CLI to process tasks /// An executor that uses Claude CLI to process tasks
pub struct ClaudeExecutor; pub struct ClaudeExecutor;
/// An executor that resumes a Claude session
pub struct ClaudeFollowupExecutor {
pub session_id: String,
pub prompt: String,
}
#[async_trait] #[async_trait]
impl Executor for ClaudeExecutor { impl Executor for ClaudeExecutor {
async fn spawn( async fn spawn(
@@ -49,3 +55,32 @@ impl Executor for ClaudeExecutor {
Ok(child) Ok(child)
} }
} }
#[async_trait]
impl Executor for ClaudeFollowupExecutor {
async fn spawn(
&self,
pool: &sqlx::SqlitePool,
task_id: Uuid,
worktree_path: &str,
) -> Result<Child, ExecutorError> {
// Use Claude CLI with --resume flag to continue the session
let child = Command::new("claude")
.kill_on_drop(true)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir(worktree_path)
.arg(&self.prompt)
.arg("-p")
.arg("--dangerously-skip-permissions")
.arg("--verbose")
.arg("--output-format=stream-json")
.arg(format!("--resume={}", self.session_id))
.process_group(0) // Create new process group so we can kill entire tree
.spawn()
.map_err(ExecutorError::SpawnFailed)?;
Ok(child)
}
}

View File

@@ -3,7 +3,7 @@ pub mod claude;
pub mod echo; pub mod echo;
pub mod setup_script; pub mod setup_script;
pub use amp::AmpExecutor; pub use amp::{AmpExecutor, AmpFollowupExecutor};
pub use claude::ClaudeExecutor; pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor};
pub use echo::EchoExecutor; pub use echo::EchoExecutor;
pub use setup_script::SetupScriptExecutor; pub use setup_script::SetupScriptExecutor;

View File

@@ -43,6 +43,37 @@ pub enum EditorType {
Custom, Custom,
} }
// Constants for frontend
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct EditorConstants {
pub editor_types: Vec<EditorType>,
pub editor_labels: Vec<String>,
}
impl EditorConstants {
pub fn new() -> Self {
Self {
editor_types: vec![
EditorType::VSCode,
EditorType::Cursor,
EditorType::Windsurf,
EditorType::IntelliJ,
EditorType::Zed,
EditorType::Custom,
],
editor_labels: vec![
"VS Code".to_string(),
"Cursor".to_string(),
"Windsurf".to_string(),
"IntelliJ IDEA".to_string(),
"Zed".to_string(),
"Custom".to_string(),
],
}
}
}
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@@ -53,6 +53,7 @@ pub struct ExecutionProcess {
pub id: Uuid, pub id: Uuid,
pub task_attempt_id: Uuid, pub task_attempt_id: Uuid,
pub process_type: ExecutionProcessType, pub process_type: ExecutionProcessType,
pub executor_type: Option<String>, // "echo", "claude", "amp", etc. - only for CodingAgent processes
pub status: ExecutionProcessStatus, pub status: ExecutionProcessStatus,
pub command: String, pub command: String,
pub args: Option<String>, // JSON array of arguments pub args: Option<String>, // JSON array of arguments
@@ -71,6 +72,7 @@ pub struct ExecutionProcess {
pub struct CreateExecutionProcess { pub struct CreateExecutionProcess {
pub task_attempt_id: Uuid, pub task_attempt_id: Uuid,
pub process_type: ExecutionProcessType, pub process_type: ExecutionProcessType,
pub executor_type: Option<String>,
pub command: String, pub command: String,
pub args: Option<String>, pub args: Option<String>,
pub working_directory: String, pub working_directory: String,
@@ -93,6 +95,7 @@ impl ExecutionProcess {
id as "id!: Uuid", id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid", task_attempt_id as "task_attempt_id!: Uuid",
process_type as "process_type!: ExecutionProcessType", process_type as "process_type!: ExecutionProcessType",
executor_type,
status as "status!: ExecutionProcessStatus", status as "status!: ExecutionProcessStatus",
command, command,
args, args,
@@ -123,6 +126,7 @@ impl ExecutionProcess {
id as "id!: Uuid", id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid", task_attempt_id as "task_attempt_id!: Uuid",
process_type as "process_type!: ExecutionProcessType", process_type as "process_type!: ExecutionProcessType",
executor_type,
status as "status!: ExecutionProcessStatus", status as "status!: ExecutionProcessStatus",
command, command,
args, args,
@@ -151,6 +155,7 @@ impl ExecutionProcess {
id as "id!: Uuid", id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid", task_attempt_id as "task_attempt_id!: Uuid",
process_type as "process_type!: ExecutionProcessType", process_type as "process_type!: ExecutionProcessType",
executor_type,
status as "status!: ExecutionProcessStatus", status as "status!: ExecutionProcessStatus",
command, command,
args, args,
@@ -181,15 +186,16 @@ impl ExecutionProcess {
sqlx::query_as!( sqlx::query_as!(
ExecutionProcess, ExecutionProcess,
r#"INSERT INTO execution_processes ( r#"INSERT INTO execution_processes (
id, task_attempt_id, process_type, status, command, args, id, task_attempt_id, process_type, executor_type, status, command, args,
working_directory, stdout, stderr, exit_code, started_at, working_directory, stdout, stderr, exit_code, started_at,
completed_at, created_at, updated_at completed_at, created_at, updated_at
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING RETURNING
id as "id!: Uuid", id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid", task_attempt_id as "task_attempt_id!: Uuid",
process_type as "process_type!: ExecutionProcessType", process_type as "process_type!: ExecutionProcessType",
executor_type,
status as "status!: ExecutionProcessStatus", status as "status!: ExecutionProcessStatus",
command, command,
args, args,
@@ -204,6 +210,7 @@ impl ExecutionProcess {
process_id, process_id,
data.task_attempt_id, data.task_attempt_id,
data.process_type, data.process_type,
data.executor_type,
ExecutionProcessStatus::Running, ExecutionProcessStatus::Running,
data.command, data.command,
data.args, data.args,

View File

@@ -0,0 +1,189 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use ts_rs::TS;
use uuid::Uuid;
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ExecutorSession {
pub id: Uuid,
pub task_attempt_id: Uuid,
pub execution_process_id: Uuid,
pub session_id: Option<String>, // External session ID from Claude/Amp
pub prompt: Option<String>, // The prompt sent to the executor
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize, TS)]
#[ts(export)]
pub struct CreateExecutorSession {
pub task_attempt_id: Uuid,
pub execution_process_id: Uuid,
pub prompt: Option<String>,
}
#[derive(Debug, Deserialize, TS)]
#[ts(export)]
pub struct UpdateExecutorSession {
pub session_id: Option<String>,
pub prompt: Option<String>,
}
impl ExecutorSession {
/// Find executor session by ID
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!(
ExecutorSession,
r#"SELECT
id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid",
execution_process_id as "execution_process_id!: Uuid",
session_id,
prompt,
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM executor_sessions
WHERE id = $1"#,
id
)
.fetch_optional(pool)
.await
}
/// Find executor session by execution process ID
pub async fn find_by_execution_process_id(
pool: &SqlitePool,
execution_process_id: Uuid,
) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!(
ExecutorSession,
r#"SELECT
id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid",
execution_process_id as "execution_process_id!: Uuid",
session_id,
prompt,
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM executor_sessions
WHERE execution_process_id = $1"#,
execution_process_id
)
.fetch_optional(pool)
.await
}
/// Find all executor sessions for a task attempt
pub async fn find_by_task_attempt_id(
pool: &SqlitePool,
task_attempt_id: Uuid,
) -> Result<Vec<Self>, sqlx::Error> {
sqlx::query_as!(
ExecutorSession,
r#"SELECT
id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid",
execution_process_id as "execution_process_id!: Uuid",
session_id,
prompt,
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM executor_sessions
WHERE task_attempt_id = $1
ORDER BY created_at ASC"#,
task_attempt_id
)
.fetch_all(pool)
.await
}
/// Create a new executor session
pub async fn create(
pool: &SqlitePool,
data: &CreateExecutorSession,
session_id: Uuid,
) -> Result<Self, sqlx::Error> {
let now = Utc::now();
sqlx::query_as!(
ExecutorSession,
r#"INSERT INTO executor_sessions (
id, task_attempt_id, execution_process_id, session_id, prompt,
created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING
id as "id!: Uuid",
task_attempt_id as "task_attempt_id!: Uuid",
execution_process_id as "execution_process_id!: Uuid",
session_id,
prompt,
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>""#,
session_id,
data.task_attempt_id,
data.execution_process_id,
None::<String>, // session_id initially None until parsed from output
data.prompt,
now, // created_at
now // updated_at
)
.fetch_one(pool)
.await
}
/// Update executor session with external session ID
pub async fn update_session_id(
pool: &SqlitePool,
execution_process_id: Uuid,
external_session_id: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"UPDATE executor_sessions
SET session_id = $1, updated_at = datetime('now')
WHERE execution_process_id = $2"#,
external_session_id,
execution_process_id
)
.execute(pool)
.await?;
Ok(())
}
/// Update executor session prompt
pub async fn update_prompt(
pool: &SqlitePool,
id: Uuid,
prompt: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"UPDATE executor_sessions
SET prompt = $1, updated_at = datetime('now')
WHERE id = $2"#,
prompt,
id
)
.execute(pool)
.await?;
Ok(())
}
/// Delete executor sessions for a task attempt (cleanup)
pub async fn delete_by_task_attempt_id(
pool: &SqlitePool,
task_attempt_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"DELETE FROM executor_sessions WHERE task_attempt_id = $1",
task_attempt_id
)
.execute(pool)
.await?;
Ok(())
}
}

View File

@@ -1,6 +1,7 @@
pub mod api_response; pub mod api_response;
pub mod config; pub mod config;
pub mod execution_process; pub mod execution_process;
pub mod executor_session;
pub mod project; pub mod project;
pub mod task; pub mod task;
pub mod task_attempt; pub mod task_attempt;

View File

@@ -82,6 +82,12 @@ pub struct UpdateTaskAttempt {
// Currently no updateable fields, but keeping struct for API compatibility // Currently no updateable fields, but keeping struct for API compatibility
} }
#[derive(Debug, Deserialize, TS)]
#[ts(export)]
pub struct CreateFollowUpAttempt {
pub prompt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
pub enum DiffChunkType { pub enum DiffChunkType {
@@ -376,10 +382,37 @@ impl TaskAttempt {
task_id: Uuid, task_id: Uuid,
project_id: Uuid, project_id: Uuid,
) -> Result<(), TaskAttemptError> { ) -> Result<(), TaskAttemptError> {
use crate::models::project::Project;
use crate::models::task::{Task, TaskStatus}; use crate::models::task::{Task, TaskStatus};
// Get the task attempt, task, and project // Load required entities
let (task_attempt, project) =
Self::load_execution_context(pool, attempt_id, project_id).await?;
// Update task status to indicate execution has started
Task::update_status(pool, task_id, project_id, TaskStatus::InProgress).await?;
// Determine execution sequence based on project configuration
if Self::should_run_setup_script(&project) {
Self::start_setup_script(
pool,
app_state,
attempt_id,
task_id,
&project,
&task_attempt.worktree_path,
)
.await
} else {
Self::start_coding_agent(pool, app_state, attempt_id, task_id, project_id).await
}
}
/// Load the execution context (task attempt and project) with validation
async fn load_execution_context(
pool: &SqlitePool,
attempt_id: Uuid,
project_id: Uuid,
) -> Result<(TaskAttempt, Project), TaskAttemptError> {
let task_attempt = TaskAttempt::find_by_id(pool, attempt_id) let task_attempt = TaskAttempt::find_by_id(pool, attempt_id)
.await? .await?
.ok_or(TaskAttemptError::TaskNotFound)?; .ok_or(TaskAttemptError::TaskNotFound)?;
@@ -388,36 +421,149 @@ impl TaskAttempt {
.await? .await?
.ok_or(TaskAttemptError::ProjectNotFound)?; .ok_or(TaskAttemptError::ProjectNotFound)?;
// Update task status to InProgress at the start of execution (during setup) Ok((task_attempt, project))
Task::update_status(pool, task_id, project_id, TaskStatus::InProgress).await?;
// Step 1: Run setup script if it exists
if let Some(setup_script) = &project.setup_script {
if !setup_script.trim().is_empty() {
Self::start_process_execution(
pool,
app_state,
attempt_id,
task_id,
crate::executor::ExecutorType::SetupScript(setup_script.clone()),
"Starting setup script".to_string(),
TaskAttemptStatus::SetupRunning,
crate::models::execution_process::ExecutionProcessType::SetupScript,
&task_attempt.worktree_path,
)
.await?;
// Wait for setup script to complete before starting executor
// We'll let the execution monitor handle the completion and then start the executor
return Ok(());
}
}
// If no setup script, start executor directly
Self::start_coding_agent(pool, app_state, attempt_id, task_id, project_id).await
} }
/// Unified function to start any type of process execution (setup script or coding agent) /// Check if setup script should be executed
fn should_run_setup_script(project: &Project) -> bool {
project
.setup_script
.as_ref()
.map(|script| !script.trim().is_empty())
.unwrap_or(false)
}
/// Start the setup script execution
async fn start_setup_script(
pool: &SqlitePool,
app_state: &crate::app_state::AppState,
attempt_id: Uuid,
task_id: Uuid,
project: &Project,
worktree_path: &str,
) -> Result<(), TaskAttemptError> {
let setup_script = project.setup_script.as_ref().unwrap();
Self::start_process_execution(
pool,
app_state,
attempt_id,
task_id,
crate::executor::ExecutorType::SetupScript(setup_script.clone()),
"Starting setup script".to_string(),
TaskAttemptStatus::SetupRunning,
crate::models::execution_process::ExecutionProcessType::SetupScript,
worktree_path,
)
.await
}
/// Start the coding agent after setup is complete or if no setup is needed
pub async fn start_coding_agent(
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)?;
let executor_config = Self::resolve_executor_config(&task_attempt.executor);
Self::start_process_execution(
pool,
app_state,
attempt_id,
task_id,
crate::executor::ExecutorType::CodingAgent(executor_config),
"Starting executor".to_string(),
TaskAttemptStatus::ExecutorRunning,
crate::models::execution_process::ExecutionProcessType::CodingAgent,
&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,
app_state: &crate::app_state::AppState,
attempt_id: Uuid,
task_id: Uuid,
project_id: Uuid,
prompt: &str,
) -> Result<(), TaskAttemptError> {
use crate::models::executor_session::ExecutorSession;
// Get task attempt
let task_attempt = TaskAttempt::find_by_id(pool, attempt_id)
.await?
.ok_or(TaskAttemptError::TaskNotFound)?;
// Find the most recent coding agent execution process to get the executor type
let execution_processes =
crate::models::execution_process::ExecutionProcess::find_by_task_attempt_id(
pool, attempt_id,
)
.await?;
let most_recent_coding_agent = execution_processes
.iter()
.rev() // Reverse to get most recent first (since they're ordered by created_at ASC)
.find(|p| {
matches!(
p.process_type,
crate::models::execution_process::ExecutionProcessType::CodingAgent
)
})
.ok_or(TaskAttemptError::TaskNotFound)?; // No previous coding agent found
// Get the executor session to find the session ID
let executor_session =
ExecutorSession::find_by_execution_process_id(pool, most_recent_coding_agent.id)
.await?
.ok_or(TaskAttemptError::TaskNotFound)?; // No session found
// Determine the executor config from the stored executor_type
let executor_config = match most_recent_coding_agent.executor_type.as_deref() {
Some("claude") => crate::executor::ExecutorConfig::Claude,
Some("amp") => crate::executor::ExecutorConfig::Amp,
Some("echo") => crate::executor::ExecutorConfig::Echo,
_ => return Err(TaskAttemptError::TaskNotFound), // Invalid executor type
};
// Create the follow-up executor type
let followup_executor = crate::executor::ExecutorType::FollowUpCodingAgent {
config: executor_config,
session_id: executor_session.session_id.clone(),
prompt: prompt.to_string(),
};
Self::start_process_execution(
pool,
app_state,
attempt_id,
task_id,
followup_executor,
"Starting follow-up executor".to_string(),
TaskAttemptStatus::ExecutorRunning,
crate::models::execution_process::ExecutionProcessType::CodingAgent,
&task_attempt.worktree_path,
)
.await
}
/// Resolve executor configuration from string name
fn resolve_executor_config(executor_name: &Option<String>) -> crate::executor::ExecutorConfig {
match executor_name.as_ref().map(|s| s.as_str()) {
Some("claude") => crate::executor::ExecutorConfig::Claude,
Some("amp") => crate::executor::ExecutorConfig::Amp,
_ => crate::executor::ExecutorConfig::Echo, // Default for "echo" or None
}
}
/// Unified function to start any type of process execution
async fn start_process_execution( async fn start_process_execution(
pool: &SqlitePool, pool: &SqlitePool,
app_state: &crate::app_state::AppState, app_state: &crate::app_state::AppState,
@@ -429,49 +575,182 @@ impl TaskAttempt {
process_type: crate::models::execution_process::ExecutionProcessType, process_type: crate::models::execution_process::ExecutionProcessType,
worktree_path: &str, worktree_path: &str,
) -> Result<(), TaskAttemptError> { ) -> Result<(), TaskAttemptError> {
use crate::executors::SetupScriptExecutor;
use crate::models::execution_process::{CreateExecutionProcess, ExecutionProcess};
use crate::models::task_attempt_activity::{
CreateTaskAttemptActivity, TaskAttemptActivity,
};
// Create execution process record first (since activity now references it)
let process_id = Uuid::new_v4(); let process_id = Uuid::new_v4();
let (command, args) = match &executor_type {
// Create execution process record
let _execution_process = Self::create_execution_process_record(
pool,
attempt_id,
process_id,
&executor_type,
process_type.clone(),
worktree_path,
)
.await?;
// Create executor session for coding agents
if matches!(
process_type,
crate::models::execution_process::ExecutionProcessType::CodingAgent
) {
Self::create_executor_session_record(pool, attempt_id, task_id, process_id).await?;
}
// Create activity record
Self::create_activity_record(pool, process_id, activity_status.clone(), &activity_note)
.await?;
tracing::info!("Starting {} for task attempt {}", activity_note, attempt_id);
// Execute the process
let child = Self::execute_process(
&executor_type,
pool,
task_id,
attempt_id,
process_id,
worktree_path,
)
.await?;
// Register for monitoring
Self::register_for_monitoring(app_state, process_id, attempt_id, &process_type, child)
.await;
tracing::info!(
"Started execution {} for task attempt {}",
process_id,
attempt_id
);
Ok(())
}
/// Create execution process database record
async fn create_execution_process_record(
pool: &SqlitePool,
attempt_id: Uuid,
process_id: Uuid,
executor_type: &crate::executor::ExecutorType,
process_type: crate::models::execution_process::ExecutionProcessType,
worktree_path: &str,
) -> Result<crate::models::execution_process::ExecutionProcess, TaskAttemptError> {
use crate::models::execution_process::{CreateExecutionProcess, ExecutionProcess};
let (command, args, executor_type_string) = match executor_type {
crate::executor::ExecutorType::SetupScript(_) => ( crate::executor::ExecutorType::SetupScript(_) => (
"bash".to_string(), "bash".to_string(),
Some(serde_json::to_string(&["-c", "setup_script"]).unwrap()), Some(serde_json::to_string(&["-c", "setup_script"]).unwrap()),
None, // Setup scripts don't have an executor type
), ),
crate::executor::ExecutorType::CodingAgent(_) => ("executor".to_string(), None), crate::executor::ExecutorType::CodingAgent(config) => {
let executor_type_str = match config {
crate::executor::ExecutorConfig::Echo => "echo",
crate::executor::ExecutorConfig::Claude => "claude",
crate::executor::ExecutorConfig::Amp => "amp",
};
(
"executor".to_string(),
None,
Some(executor_type_str.to_string()),
)
}
crate::executor::ExecutorType::FollowUpCodingAgent { config, .. } => {
let executor_type_str = match config {
crate::executor::ExecutorConfig::Echo => "echo",
crate::executor::ExecutorConfig::Claude => "claude",
crate::executor::ExecutorConfig::Amp => "amp",
};
(
"followup_executor".to_string(),
None,
Some(executor_type_str.to_string()),
)
}
}; };
let create_process = CreateExecutionProcess { let create_process = CreateExecutionProcess {
task_attempt_id: attempt_id, task_attempt_id: attempt_id,
process_type: process_type.clone(), process_type,
executor_type: executor_type_string,
command, command,
args, args,
working_directory: worktree_path.to_string(), working_directory: worktree_path.to_string(),
}; };
let _process = ExecutionProcess::create(pool, &create_process, process_id).await?; ExecutionProcess::create(pool, &create_process, process_id)
.await
.map_err(TaskAttemptError::from)
}
/// Create executor session record for coding agents
async fn create_executor_session_record(
pool: &SqlitePool,
attempt_id: Uuid,
task_id: Uuid,
process_id: Uuid,
) -> Result<(), TaskAttemptError> {
use crate::models::executor_session::{CreateExecutorSession, ExecutorSession};
// Get the task to create prompt
let task = Task::find_by_id(pool, task_id)
.await?
.ok_or(TaskAttemptError::TaskNotFound)?;
let prompt = format!("{}\n\n{}", task.title, task.description.unwrap_or_default());
let session_id = Uuid::new_v4();
let create_session = CreateExecutorSession {
task_attempt_id: attempt_id,
execution_process_id: process_id,
prompt: Some(prompt),
};
ExecutorSession::create(pool, &create_session, session_id)
.await
.map(|_| ())
.map_err(TaskAttemptError::from)
}
/// Create activity record for process start
async fn create_activity_record(
pool: &SqlitePool,
process_id: Uuid,
activity_status: TaskAttemptStatus,
activity_note: &str,
) -> Result<(), TaskAttemptError> {
use crate::models::task_attempt_activity::{
CreateTaskAttemptActivity, TaskAttemptActivity,
};
// Create activity for process start (after process is created)
let activity_id = Uuid::new_v4(); let activity_id = Uuid::new_v4();
let create_activity = CreateTaskAttemptActivity { let create_activity = CreateTaskAttemptActivity {
execution_process_id: process_id, execution_process_id: process_id,
status: Some(activity_status.clone()), status: Some(activity_status.clone()),
note: Some(activity_note.clone()), note: Some(activity_note.to_string()),
}; };
TaskAttemptActivity::create(pool, &create_activity, activity_id, activity_status.clone()) TaskAttemptActivity::create(pool, &create_activity, activity_id, activity_status)
.await?; .await
.map(|_| ())
.map_err(TaskAttemptError::from)
}
tracing::info!("Starting {} for task attempt {}", activity_note, attempt_id); /// Execute the process based on type
async fn execute_process(
executor_type: &crate::executor::ExecutorType,
pool: &SqlitePool,
task_id: Uuid,
attempt_id: Uuid,
process_id: Uuid,
worktree_path: &str,
) -> Result<tokio::process::Child, TaskAttemptError> {
use crate::executors::SetupScriptExecutor;
// Create the appropriate executor and spawn the process let result = match executor_type {
let child = match executor_type {
crate::executor::ExecutorType::SetupScript(script) => { crate::executor::ExecutorType::SetupScript(script) => {
let executor = SetupScriptExecutor { script }; let executor = SetupScriptExecutor {
script: script.clone(),
};
executor executor
.execute_streaming(pool, task_id, attempt_id, process_id, worktree_path) .execute_streaming(pool, task_id, attempt_id, process_id, worktree_path)
.await .await
@@ -482,10 +761,57 @@ impl TaskAttempt {
.execute_streaming(pool, task_id, attempt_id, process_id, worktree_path) .execute_streaming(pool, task_id, attempt_id, process_id, worktree_path)
.await .await
} }
} crate::executor::ExecutorType::FollowUpCodingAgent {
.map_err(|e| TaskAttemptError::Git(git2::Error::from_str(&e.to_string())))?; config,
session_id,
prompt,
} => {
use crate::executors::{AmpFollowupExecutor, ClaudeFollowupExecutor};
// Add to running executions for monitoring let executor: Box<dyn crate::executor::Executor> = match config {
crate::executor::ExecutorConfig::Claude => {
if let Some(sid) = session_id {
Box::new(ClaudeFollowupExecutor {
session_id: sid.clone(),
prompt: prompt.clone(),
})
} else {
return Err(TaskAttemptError::TaskNotFound); // No session ID for followup
}
}
crate::executor::ExecutorConfig::Amp => {
if let Some(tid) = session_id {
Box::new(AmpFollowupExecutor {
thread_id: tid.clone(),
prompt: prompt.clone(),
})
} else {
return Err(TaskAttemptError::TaskNotFound); // No thread ID for followup
}
}
crate::executor::ExecutorConfig::Echo => {
// Echo doesn't support followup, use regular echo
config.create_executor()
}
};
executor
.execute_streaming(pool, task_id, attempt_id, process_id, worktree_path)
.await
}
};
result.map_err(|e| TaskAttemptError::Git(git2::Error::from_str(&e.to_string())))
}
/// Register process for monitoring
async fn register_for_monitoring(
app_state: &crate::app_state::AppState,
process_id: Uuid,
attempt_id: Uuid,
process_type: &crate::models::execution_process::ExecutionProcessType,
child: tokio::process::Child,
) {
let execution_type = match process_type { let execution_type = match process_type {
crate::models::execution_process::ExecutionProcessType::SetupScript => { crate::models::execution_process::ExecutionProcessType::SetupScript => {
crate::app_state::ExecutionType::SetupScript crate::app_state::ExecutionType::SetupScript
@@ -508,53 +834,6 @@ impl TaskAttempt {
}, },
) )
.await; .await;
tracing::info!(
"Started execution {} for task attempt {}",
process_id,
attempt_id
);
Ok(())
}
/// Start the coding agent after setup is complete or if no setup is needed
pub async fn start_coding_agent(
pool: &SqlitePool,
app_state: &crate::app_state::AppState,
attempt_id: Uuid,
task_id: Uuid,
_project_id: Uuid,
) -> Result<(), TaskAttemptError> {
// Get the task attempt to determine executor config
let task_attempt = TaskAttempt::find_by_id(pool, attempt_id)
.await?
.ok_or(TaskAttemptError::TaskNotFound)?;
// Determine the executor config
let executor_config = if let Some(executor_name) = &task_attempt.executor {
match executor_name.as_str() {
"echo" => crate::executor::ExecutorConfig::Echo,
"claude" => crate::executor::ExecutorConfig::Claude,
"amp" => crate::executor::ExecutorConfig::Amp,
_ => crate::executor::ExecutorConfig::Echo, // Default fallback
}
} else {
crate::executor::ExecutorConfig::Echo // Default
};
Self::start_process_execution(
pool,
app_state,
attempt_id,
task_id,
crate::executor::ExecutorType::CodingAgent(executor_config),
"Starting executor".to_string(),
TaskAttemptStatus::ExecutorRunning,
crate::models::execution_process::ExecutionProcessType::CodingAgent,
&task_attempt.worktree_path,
)
.await
} }
/// Get the git diff between the base commit and the current committed worktree state /// Get the git diff between the base commit and the current committed worktree state

View File

@@ -13,7 +13,10 @@ use uuid::Uuid;
use crate::models::{ use crate::models::{
execution_process::ExecutionProcess, execution_process::ExecutionProcess,
task::Task, task::Task,
task_attempt::{BranchStatus, CreateTaskAttempt, TaskAttempt, TaskAttemptStatus, WorktreeDiff}, task_attempt::{
BranchStatus, CreateFollowUpAttempt, CreateTaskAttempt, TaskAttempt, TaskAttemptStatus,
WorktreeDiff,
},
task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity},
ApiResponse, ApiResponse,
}; };
@@ -746,6 +749,53 @@ pub async fn delete_task_attempt_file(
} }
} }
pub async fn create_followup_attempt(
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
Extension(pool): Extension<SqlitePool>,
Extension(app_state): Extension<crate::app_state::AppState>,
Json(payload): Json<CreateFollowUpAttempt>,
) -> Result<ResponseJson<ApiResponse<String>>, StatusCode> {
// Verify task attempt exists
if !TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id)
.await
.map_err(|e| {
tracing::error!("Failed to check task attempt existence: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?
{
return Err(StatusCode::NOT_FOUND);
}
// Start follow-up execution asynchronously
let pool_clone = pool.clone();
let app_state_clone = app_state.clone();
let prompt = payload.prompt.clone();
tokio::spawn(async move {
if let Err(e) = TaskAttempt::start_followup_execution(
&pool_clone,
&app_state_clone,
attempt_id,
task_id,
project_id,
&prompt,
)
.await
{
tracing::error!(
"Failed to start follow-up execution for task attempt {}: {}",
attempt_id,
e
);
}
});
Ok(ResponseJson(ApiResponse {
success: true,
data: Some("Follow-up execution started successfully".to_string()),
message: Some("Follow-up execution started successfully".to_string()),
}))
}
pub fn task_attempts_router() -> Router { pub fn task_attempts_router() -> Router {
use axum::routing::post; use axum::routing::post;
@@ -799,4 +849,8 @@ pub fn task_attempts_router() -> Router {
"/projects/:project_id/execution-processes/:process_id", "/projects/:project_id/execution-processes/:process_id",
get(get_execution_process), get(get_execution_process),
) )
.route(
"/projects/:project_id/tasks/:task_id/attempts/:attempt_id/follow-up",
post(create_followup_attempt),
)
} }

View File

@@ -12,11 +12,13 @@ import {
Edit, Edit,
Trash2, Trash2,
StopCircle, StopCircle,
Send,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Chip } from "@/components/ui/chip"; import { Chip } from "@/components/ui/chip";
import { Textarea } from "@/components/ui/textarea";
import { ExecutionOutputViewer } from "./ExecutionOutputViewer"; import { ExecutionOutputViewer } from "./ExecutionOutputViewer";
import { EditorSelectionDialog } from "./EditorSelectionDialog"; import { EditorSelectionDialog } from "./EditorSelectionDialog";
@@ -146,6 +148,8 @@ export function TaskDetailsPanel({
new Set() new Set()
); );
const [showEditorDialog, setShowEditorDialog] = useState(false); const [showEditorDialog, setShowEditorDialog] = useState(false);
const [followUpMessage, setFollowUpMessage] = useState("");
const [isSendingFollowUp, setIsSendingFollowUp] = useState(false);
const { config } = useConfig(); const { config } = useConfig();
// Available executors // Available executors
@@ -185,6 +189,20 @@ export function TaskDetailsPanel({
); );
}, [selectedAttempt, attemptActivities, isStopping]); }, [selectedAttempt, attemptActivities, isStopping]);
// Check if follow-up should be enabled
const canSendFollowUp = useMemo(() => {
if (!selectedAttempt || attemptActivities.length === 0 || isAttemptRunning || isSendingFollowUp) {
return false;
}
// Need at least one completed coding agent execution
const codingAgentActivities = attemptActivities.filter(
(activity) => activity.status === "executorcomplete"
);
return codingAgentActivities.length > 0;
}, [selectedAttempt, attemptActivities, isAttemptRunning, isSendingFollowUp]);
// Polling for updates when attempt is running // Polling for updates when attempt is running
useEffect(() => { useEffect(() => {
if (!isAttemptRunning || !task) return; if (!isAttemptRunning || !task) return;
@@ -410,6 +428,39 @@ export function TaskDetailsPanel({
}); });
}; };
const handleSendFollowUp = async () => {
if (!task || !selectedAttempt || !followUpMessage.trim()) return;
try {
setIsSendingFollowUp(true);
const response = await makeRequest(
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/follow-up`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: followUpMessage.trim(),
}),
}
);
if (response.ok) {
// Clear the message
setFollowUpMessage("");
// Refresh activities to show the new follow-up execution
fetchAttemptActivities(selectedAttempt.id);
} else {
console.error("Failed to send follow-up:", await response.text());
}
} catch (err) {
console.error("Failed to send follow-up:", err);
} finally {
setIsSendingFollowUp(false);
}
};
if (!task) return null; if (!task) return null;
return ( return (
@@ -730,13 +781,13 @@ export function TaskDetailsPanel({
] && ( ] && (
<div className="mt-2"> <div className="mt-2">
<div <div
className={`transition-all duration-200 ${ className={`transition-all duration-200 ${
expandedOutputs.has( expandedOutputs.has(
activity.execution_process_id activity.execution_process_id
) )
? "" ? ""
: "max-h-64 overflow-hidden" : "max-h-64 overflow-hidden flex flex-col justify-end"
}`} }`}
> >
<ExecutionOutputViewer <ExecutionOutputViewer
executionProcess={ executionProcess={
@@ -786,32 +837,51 @@ export function TaskDetailsPanel({
)} )}
</div> </div>
{/* Footer */} {/* Footer - Follow-up section */}
{/* <div className="border-t p-4"> {selectedAttempt && (
<div className="space-y-2"> <div className="border-t p-4">
<Label className="text-sm font-medium"> <div className="space-y-2">
Follow-up question <Label className="text-sm font-medium">
</Label> Follow-up question
<div className="flex gap-2"> </Label>
<Textarea <div className="flex gap-2">
placeholder="Ask a follow-up question about this task..." <Textarea
value={followUpMessage} placeholder="Ask a follow-up question about this task..."
onChange={(e) => setFollowUpMessage(e.target.value)} value={followUpMessage}
className="flex-1 min-h-[60px] resize-none" onChange={(e) => setFollowUpMessage(e.target.value)}
/> onKeyDown={(e) => {
<Button if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
onClick={handleSendFollowUp} e.preventDefault();
disabled={!followUpMessage.trim()} if (canSendFollowUp && followUpMessage.trim() && !isSendingFollowUp) {
className="self-end" handleSendFollowUp();
> }
<Send className="h-4 w-4" /> }
</Button> }}
className="flex-1 min-h-[60px] resize-none"
disabled={!canSendFollowUp}
/>
<Button
onClick={handleSendFollowUp}
disabled={!canSendFollowUp || !followUpMessage.trim() || isSendingFollowUp}
className="self-end"
>
{isSendingFollowUp ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{!canSendFollowUp
? isAttemptRunning
? "Wait for current execution to complete before asking follow-up questions"
: "Complete at least one coding agent execution to enable follow-up questions"
: "Continue the conversation with the most recent executor session"}
</p>
</div> </div>
<p className="text-xs text-muted-foreground">
Follow-up functionality coming soon
</p>
</div> </div>
</div> */} )}
</div> </div>
</div> </div>

View File

@@ -12,26 +12,11 @@ export type EditorConfig = { editor_type: EditorType, custom_command: string | n
export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom"; export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom";
export type EditorConstants = { editor_types: Array<EditorType>, editor_labels: Array<string>, };
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" }; export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
// Constants for UI components export type ExecutorConstants = { executor_types: Array<ExecutorConfig>, executor_labels: Array<string>, };
export const EXECUTOR_TYPES = ["echo", "claude", "amp"] as const;
export const EDITOR_TYPES = ["vscode", "cursor", "windsurf", "intellij", "zed", "custom"] as const;
export const EXECUTOR_LABELS = {
echo: "Echo",
claude: "Claude",
amp: "Amp"
} as const;
export const EDITOR_LABELS = {
vscode: "VS Code",
cursor: "Cursor",
windsurf: "Windsurf",
intellij: "IntelliJ IDEA",
zed: "Zed",
custom: "Custom Command"
} as const;
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, };
@@ -63,6 +48,8 @@ export type CreateTaskAttempt = { executor: string | null, };
export type UpdateTaskAttempt = Record<string, never>; export type UpdateTaskAttempt = Record<string, never>;
export type CreateFollowUpAttempt = { prompt: string, };
export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, }; export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, }; export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, };
@@ -79,12 +66,49 @@ export type WorktreeDiff = { files: Array<FileDiff>, };
export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, }; export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, };
export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, 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 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 ExecutionProcessStatus = "running" | "completed" | "failed" | "killed"; export type ExecutionProcessStatus = "running" | "completed" | "failed" | "killed";
export type ExecutionProcessType = "setupscript" | "codingagent" | "devserver"; export type ExecutionProcessType = "setupscript" | "codingagent" | "devserver";
export type CreateExecutionProcess = { task_attempt_id: string, process_type: ExecutionProcessType, command: string, args: string | null, working_directory: string, }; export type CreateExecutionProcess = { task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, command: string, args: string | null, working_directory: string, };
export type UpdateExecutionProcess = { status: ExecutionProcessStatus | null, exit_code: bigint | null, completed_at: string | null, }; export type UpdateExecutionProcess = { status: ExecutionProcessStatus | null, exit_code: bigint | null, completed_at: string | null, };
export type ExecutorSession = { id: string, task_attempt_id: string, execution_process_id: string, session_id: string | null, prompt: string | null, created_at: string, updated_at: string, };
export type CreateExecutorSession = { task_attempt_id: string, execution_process_id: string, prompt: string | null, };
export type UpdateExecutorSession = { session_id: string | null, prompt: string | null, };
// Generated constants
export const EXECUTOR_TYPES: string[] = [
"echo",
"claude",
"amp"
];
export const EDITOR_TYPES: EditorType[] = [
"vscode",
"cursor",
"windsurf",
"intellij",
"zed",
"custom"
];
export const EXECUTOR_LABELS: Record<string, string> = {
"echo": "Echo (Test Mode)",
"claude": "Claude",
"amp": "Amp"
};
export const EDITOR_LABELS: Record<string, string> = {
"vscode": "VS Code",
"cursor": "Cursor",
"windsurf": "Windsurf",
"intellij": "IntelliJ IDEA",
"zed": "Zed",
"custom": "Custom"
};