Workspaces FE (#1733)
80
crates/db/.sqlx/query-10f7e20c57dd336fbd405a1080722a0d6ca5b304e88a9fdacacf6c85de00b81f.json
generated
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n w.id as \"id!: Uuid\",\n w.task_id as \"task_id!: Uuid\",\n w.container_ref,\n w.branch as \"branch!\",\n w.agent_working_dir,\n w.setup_completed_at as \"setup_completed_at: DateTime<Utc>\",\n w.created_at as \"created_at!: DateTime<Utc>\",\n w.updated_at as \"updated_at!: DateTime<Utc>\",\n w.archived as \"archived!: bool\",\n w.pinned as \"pinned!: bool\",\n w.name\n FROM workspaces w\n LEFT JOIN sessions s ON w.id = s.workspace_id\n LEFT JOIN execution_processes ep ON s.id = ep.session_id AND ep.completed_at IS NOT NULL\n WHERE w.container_ref IS NOT NULL\n AND w.id NOT IN (\n SELECT DISTINCT s2.workspace_id\n FROM sessions s2\n JOIN execution_processes ep2 ON s2.id = ep2.session_id\n WHERE ep2.completed_at IS NULL\n )\n GROUP BY w.id, w.container_ref, w.updated_at\n HAVING datetime('now', '-72 hours') > datetime(\n MAX(\n CASE\n WHEN ep.completed_at IS NOT NULL THEN ep.completed_at\n ELSE w.updated_at\n END\n )\n )\n ORDER BY MAX(\n CASE\n WHEN ep.completed_at IS NOT NULL THEN ep.completed_at\n ELSE w.updated_at\n END\n ) ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "task_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "container_ref",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "branch!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "agent_working_dir",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "setup_completed_at: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "10f7e20c57dd336fbd405a1080722a0d6ca5b304e88a9fdacacf6c85de00b81f"
|
||||
}
|
||||
92
crates/db/.sqlx/query-1a866e787ddc0731b88a4fb86470b116b8f8e2b80a29b1b83acdc6a454ea4038.json
generated
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n w.id AS \"id!: Uuid\",\n w.task_id AS \"task_id!: Uuid\",\n w.container_ref,\n w.branch,\n w.agent_working_dir,\n w.setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n w.created_at AS \"created_at!: DateTime<Utc>\",\n w.updated_at AS \"updated_at!: DateTime<Utc>\",\n w.archived AS \"archived!: bool\",\n w.pinned AS \"pinned!: bool\",\n w.name,\n\n CASE WHEN EXISTS (\n SELECT 1\n FROM sessions s\n JOIN execution_processes ep ON ep.session_id = s.id\n WHERE s.workspace_id = w.id\n AND ep.status = 'running'\n AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')\n LIMIT 1\n ) THEN 1 ELSE 0 END AS \"is_running!: i64\",\n\n CASE WHEN (\n SELECT ep.status\n FROM sessions s\n JOIN execution_processes ep ON ep.session_id = s.id\n WHERE s.workspace_id = w.id\n AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')\n ORDER BY ep.created_at DESC\n LIMIT 1\n ) IN ('failed','killed') THEN 1 ELSE 0 END AS \"is_errored!: i64\"\n\n FROM workspaces w\n WHERE w.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "task_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "container_ref",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "branch",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "agent_working_dir",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "setup_completed_at: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "is_running!: i64",
|
||||
"ordinal": 11,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "is_errored!: i64",
|
||||
"ordinal": 12,
|
||||
"type_info": "Null"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1a866e787ddc0731b88a4fb86470b116b8f8e2b80a29b1b83acdc6a454ea4038"
|
||||
}
|
||||
12
crates/db/.sqlx/query-1c2201b0ca9305283634fe5c72df6eac3ad954c1238088a84a4b9085b1dbdb74.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM workspaces WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1c2201b0ca9305283634fe5c72df6eac3ad954c1238088a84a4b9085b1dbdb74"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n prompt,\n summary,\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM coding_agent_turns\n WHERE agent_session_id = ?\n ORDER BY updated_at DESC\n LIMIT 1",
|
||||
"query": "SELECT\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n prompt,\n summary,\n seen as \"seen!: bool\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM coding_agent_turns\n WHERE agent_session_id = ?\n ORDER BY updated_at DESC\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -29,13 +29,18 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "seen!: bool",
|
||||
"ordinal": 5,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -49,8 +54,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6c4d08764c41abb08c07ffda411d6a8e3456db56d9ea6c3d71f3554afc1dd213"
|
||||
"hash": "241edef9e66f032a8a5afce586f2a78c82cf2c795d880126596554b9ce5eaf76"
|
||||
}
|
||||
44
crates/db/.sqlx/query-2462cbaa0c7bc7cc0d925b467bcc40ab4406977d1032e81371f7b2dda115f632.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT s.id AS \"id!: Uuid\",\n s.workspace_id AS \"workspace_id!: Uuid\",\n s.executor,\n s.created_at AS \"created_at!: DateTime<Utc>\",\n s.updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM sessions s\n LEFT JOIN (\n SELECT ep.session_id, MAX(ep.created_at) as last_used\n FROM execution_processes ep\n WHERE ep.run_reason != 'devserver' AND ep.dropped = FALSE\n GROUP BY ep.session_id\n ) latest_ep ON s.id = latest_ep.session_id\n WHERE s.workspace_id = $1\n ORDER BY COALESCE(latest_ep.last_used, s.created_at) DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "executor",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2462cbaa0c7bc7cc0d925b467bcc40ab4406977d1032e81371f7b2dda115f632"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM workspaces\n WHERE rowid = $1",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n archived AS \"archived!: bool\",\n pinned AS \"pinned!: bool\",\n name\n FROM workspaces\n WHERE rowid = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,21 @@
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +70,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1d84ba9ca8016f7b15d84acb3442c3b84ff344f666c31bb29223b6aaa440091f"
|
||||
"hash": "265392a2e833b8f0bc7ac317b6c82e12e935a964ae34faea00b2c7f15e988b39"
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n w.id as \"id!: Uuid\",\n w.task_id as \"task_id!: Uuid\",\n w.container_ref,\n w.branch as \"branch!\",\n w.agent_working_dir,\n w.setup_completed_at as \"setup_completed_at: DateTime<Utc>\",\n w.created_at as \"created_at!: DateTime<Utc>\",\n w.updated_at as \"updated_at!: DateTime<Utc>\"\n FROM workspaces w\n LEFT JOIN sessions s ON w.id = s.workspace_id\n LEFT JOIN execution_processes ep ON s.id = ep.session_id AND ep.completed_at IS NOT NULL\n WHERE w.container_ref IS NOT NULL\n AND w.id NOT IN (\n SELECT DISTINCT s2.workspace_id\n FROM sessions s2\n JOIN execution_processes ep2 ON s2.id = ep2.session_id\n WHERE ep2.completed_at IS NULL\n )\n GROUP BY w.id, w.container_ref, w.updated_at\n HAVING datetime('now', '-72 hours') > datetime(\n MAX(\n CASE\n WHEN ep.completed_at IS NOT NULL THEN ep.completed_at\n ELSE w.updated_at\n END\n )\n )\n ORDER BY MAX(\n CASE\n WHEN ep.completed_at IS NOT NULL THEN ep.completed_at\n ELSE w.updated_at\n END\n ) ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "task_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "container_ref",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "branch!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "agent_working_dir",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "setup_completed_at: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3d4cd3c4749b2b39e1d18f3a8e7fd38f5b5a165c796616a904acd820190df731"
|
||||
}
|
||||
12
crates/db/.sqlx/query-4d86dcbf754f971ff3acffe9e85b5c2f455e77c40c624e09736be9480238110b.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE workspaces SET archived = $1, updated_at = datetime('now', 'subsec') WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d86dcbf754f971ff3acffe9e85b5c2f455e77c40c624e09736be9480238110b"
|
||||
}
|
||||
92
crates/db/.sqlx/query-52ca593629af7f97c925d99461f29e98831442469d2faf09a49511c9eb665a9d.json
generated
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n w.id AS \"id!: Uuid\",\n w.task_id AS \"task_id!: Uuid\",\n w.container_ref,\n w.branch,\n w.agent_working_dir,\n w.setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n w.created_at AS \"created_at!: DateTime<Utc>\",\n w.updated_at AS \"updated_at!: DateTime<Utc>\",\n w.archived AS \"archived!: bool\",\n w.pinned AS \"pinned!: bool\",\n w.name,\n\n CASE WHEN EXISTS (\n SELECT 1\n FROM sessions s\n JOIN execution_processes ep ON ep.session_id = s.id\n WHERE s.workspace_id = w.id\n AND ep.status = 'running'\n AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')\n LIMIT 1\n ) THEN 1 ELSE 0 END AS \"is_running!: i64\",\n\n CASE WHEN (\n SELECT ep.status\n FROM sessions s\n JOIN execution_processes ep ON ep.session_id = s.id\n WHERE s.workspace_id = w.id\n AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')\n ORDER BY ep.created_at DESC\n LIMIT 1\n ) IN ('failed','killed') THEN 1 ELSE 0 END AS \"is_errored!: i64\"\n\n FROM workspaces w\n ORDER BY w.updated_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "task_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "container_ref",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "branch",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "agent_working_dir",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "setup_completed_at: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "is_running!: i64",
|
||||
"ordinal": 11,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "is_errored!: i64",
|
||||
"ordinal": 12,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "52ca593629af7f97c925d99461f29e98831442469d2faf09a49511c9eb665a9d"
|
||||
}
|
||||
12
crates/db/.sqlx/query-57d6335c98deb608cf961836f73a67163af6484f91954c0577aea1cc2fddac4f.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE workspaces SET\n archived = COALESCE($1, archived),\n pinned = COALESCE($2, pinned),\n name = CASE WHEN $3 THEN $4 ELSE name END,\n updated_at = datetime('now', 'subsec')\n WHERE id = $5",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "57d6335c98deb608cf961836f73a67163af6484f91954c0577aea1cc2fddac4f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO workspaces (id, task_id, container_ref, branch, agent_working_dir, setup_completed_at)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", container_ref, branch, agent_working_dir, setup_completed_at as \"setup_completed_at: DateTime<Utc>\", created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"query": "INSERT INTO workspaces (id, task_id, container_ref, branch, agent_working_dir, setup_completed_at)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", container_ref, branch, agent_working_dir, setup_completed_at as \"setup_completed_at: DateTime<Utc>\", created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\", archived as \"archived!: bool\", pinned as \"pinned!: bool\", name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,21 @@
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +70,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a672d2f33fca9a19ae68c29e5d3a14c8b050389fb55b80b3788901bc79fdde1b"
|
||||
"hash": "59567b3422999cd8cf5eccc8eb2a19e58a00712239ea078f01450491b4e6d159"
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n workspace_id AS \"workspace_id!: Uuid\",\n executor,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM sessions\n WHERE workspace_id = $1\n ORDER BY created_at DESC\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "executor",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5a8d67e33c564f02faef85a19503f204332b8353974718c96f23b2942c1fb142"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT w.id AS \"id!: Uuid\",\n w.task_id AS \"task_id!: Uuid\",\n w.container_ref,\n w.branch,\n w.agent_working_dir,\n w.setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n w.created_at AS \"created_at!: DateTime<Utc>\",\n w.updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM workspaces w\n JOIN tasks t ON w.task_id = t.id\n JOIN projects p ON t.project_id = p.id\n WHERE w.id = $1 AND t.id = $2 AND p.id = $3",
|
||||
"query": "SELECT w.id AS \"id!: Uuid\",\n w.task_id AS \"task_id!: Uuid\",\n w.container_ref,\n w.branch,\n w.agent_working_dir,\n w.setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n w.created_at AS \"created_at!: DateTime<Utc>\",\n w.updated_at AS \"updated_at!: DateTime<Utc>\",\n w.archived AS \"archived!: bool\",\n w.pinned AS \"pinned!: bool\",\n w.name\n FROM workspaces w\n JOIN tasks t ON w.task_id = t.id\n JOIN projects p ON t.project_id = p.id\n WHERE w.id = $1 AND t.id = $2 AND p.id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,21 @@
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +70,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7c6ce796263a1b9c1a9f14fc4a565063fb627263efe4303ef7f165b0b3e6b21e"
|
||||
"hash": "5cd7c1466a0555b4aa009afe31ea0dd9f37fe4d2652ee7ebf412caae3efa5e74"
|
||||
}
|
||||
20
crates/db/.sqlx/query-606016bf31cf0abd87d7eb95ea99d7c8c014523cb02e482d608299ceefaeffc5.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT EXISTS(\n SELECT 1 FROM coding_agent_turns cat\n JOIN execution_processes ep ON cat.execution_process_id = ep.id\n JOIN sessions s ON ep.session_id = s.id\n WHERE s.workspace_id = $1 AND cat.seen = 0\n ) as \"has_unseen!: bool\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "has_unseen!: bool",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "606016bf31cf0abd87d7eb95ea99d7c8c014523cb02e482d608299ceefaeffc5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n prompt,\n summary,\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM coding_agent_turns\n WHERE execution_process_id = $1",
|
||||
"query": "SELECT\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n prompt,\n summary,\n seen as \"seen!: bool\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM coding_agent_turns\n WHERE execution_process_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -29,13 +29,18 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "seen!: bool",
|
||||
"ordinal": 5,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -49,8 +54,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3237245fad0281421e131ee97de37464a7077cce1224fd45de95eb16a07fff79"
|
||||
"hash": "6950a29b30e8094180b0e927d380ea681a961c090c00f243555509fe09b4eace"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO coding_agent_turns (\n id, execution_process_id, agent_session_id, prompt, summary,\n created_at, updated_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n prompt,\n summary,\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"query": "INSERT INTO coding_agent_turns (\n id, execution_process_id, agent_session_id, prompt, summary, seen,\n created_at, updated_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n RETURNING\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n prompt,\n summary,\n seen as \"seen!: bool\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -29,18 +29,23 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"name": "seen!: bool",
|
||||
"ordinal": 5,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
@@ -49,8 +54,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "953633729bed1499b03c338d650c4b404d037cd25dda759b56f72f7fa31e9a04"
|
||||
"hash": "70154468149c2505414085d853b58c5fbe02dabacd2a11c084670011c05d98c2"
|
||||
}
|
||||
20
crates/db/.sqlx/query-82b92577d15b92908cecca7bff6ff21478c90a16eb6123165f0bd16d2d60bca4.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT DISTINCT s.workspace_id as \"workspace_id!: Uuid\"\n FROM coding_agent_turns cat\n JOIN execution_processes ep ON cat.execution_process_id = ep.id\n JOIN sessions s ON ep.session_id = s.id\n JOIN workspaces w ON s.workspace_id = w.id\n WHERE cat.seen = 0 AND w.archived = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "82b92577d15b92908cecca7bff6ff21478c90a16eb6123165f0bd16d2d60bca4"
|
||||
}
|
||||
44
crates/db/.sqlx/query-91d94506317abcd36e0da72066a5d31c3ec52530661a96c8231912c5b2a683f8.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT s.id AS \"id!: Uuid\",\n s.workspace_id AS \"workspace_id!: Uuid\",\n s.executor,\n s.created_at AS \"created_at!: DateTime<Utc>\",\n s.updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM sessions s\n LEFT JOIN (\n SELECT ep.session_id, MAX(ep.created_at) as last_used\n FROM execution_processes ep\n WHERE ep.run_reason != 'devserver' AND ep.dropped = FALSE\n GROUP BY ep.session_id\n ) latest_ep ON s.id = latest_ep.session_id\n WHERE s.workspace_id = $1\n ORDER BY COALESCE(latest_ep.last_used, s.created_at) DESC\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "executor",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "91d94506317abcd36e0da72066a5d31c3ec52530661a96c8231912c5b2a683f8"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM workspaces\n ORDER BY created_at DESC",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n archived AS \"archived!: bool\",\n pinned AS \"pinned!: bool\",\n name\n FROM workspaces\n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,21 @@
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +70,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0e51827057df2d9e78304623e44487109740a841bdab334bb8b43db60215ecb1"
|
||||
"hash": "98f17039583ca6c709108f82f633cbff865e517a4c37de6d496ce20f746e702a"
|
||||
}
|
||||
44
crates/db/.sqlx/query-a11f0e691ddde00e63e3c4aaf6bc8458a93facc016142ceb5e8b903fecd4bfde.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n s.workspace_id as \"workspace_id!: Uuid\",\n ep.id as \"execution_process_id!: Uuid\",\n ep.session_id as \"session_id!: Uuid\",\n ep.status as \"status!: ExecutionProcessStatus\",\n ep.completed_at as \"completed_at?: DateTime<Utc>\"\n FROM execution_processes ep\n JOIN sessions s ON ep.session_id = s.id\n JOIN workspaces w ON s.workspace_id = w.id\n WHERE w.archived = $1\n AND ep.run_reason IN ('codingagent', 'setupscript', 'cleanupscript')\n AND ep.dropped = FALSE\n AND ep.created_at = (\n SELECT MAX(ep2.created_at)\n FROM execution_processes ep2\n JOIN sessions s2 ON ep2.session_id = s2.id\n WHERE s2.workspace_id = s.workspace_id\n AND ep2.run_reason IN ('codingagent', 'setupscript', 'cleanupscript')\n AND ep2.dropped = FALSE\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "execution_process_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "session_id!: Uuid",
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "status!: ExecutionProcessStatus",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "completed_at?: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a11f0e691ddde00e63e3c4aaf6bc8458a93facc016142ceb5e8b903fecd4bfde"
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n workspace_id AS \"workspace_id!: Uuid\",\n executor,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM sessions\n WHERE workspace_id = $1\n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "executor",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a2d108762c773c3ee1547d3f092b3ef6fae482b028e37b8175c38b12b31115c0"
|
||||
}
|
||||
12
crates/db/.sqlx/query-ab2693e557142354564a2882f3c321b350828419c440885c0f88840079b1c94e.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE coding_agent_turns\n SET seen = 1, updated_at = $1\n WHERE execution_process_id IN (\n SELECT ep.id FROM execution_processes ep\n JOIN sessions s ON ep.session_id = s.id\n WHERE s.workspace_id = $2\n ) AND seen = 0",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ab2693e557142354564a2882f3c321b350828419c440885c0f88840079b1c94e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM workspaces\n WHERE id = $1",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n archived AS \"archived!: bool\",\n pinned AS \"pinned!: bool\",\n name\n FROM workspaces\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,21 @@
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +70,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b96fe730f54d9bea01ff6306bed580d7ad1f12d379d434898a77303d32a32efc"
|
||||
"hash": "b8d1476b4eb028290e9bc76d08b3d3c75984c558c69895393129dc45b5311151"
|
||||
}
|
||||
20
crates/db/.sqlx/query-c198aaeda2f7b7864ca2ece70ba700ce613ca135f1b11883521e8e7301515a1a.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT cat.prompt\n FROM sessions s\n JOIN execution_processes ep ON ep.session_id = s.id\n JOIN coding_agent_turns cat ON cat.execution_process_id = ep.id\n WHERE s.workspace_id = $1\n AND s.executor IS NOT NULL\n AND cat.prompt IS NOT NULL\n ORDER BY s.created_at ASC, ep.created_at ASC\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "prompt",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c198aaeda2f7b7864ca2ece70ba700ce613ca135f1b11883521e8e7301515a1a"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\"\n FROM workspaces\n WHERE task_id = $1\n ORDER BY created_at DESC",
|
||||
"query": "SELECT id AS \"id!: Uuid\",\n task_id AS \"task_id!: Uuid\",\n container_ref,\n branch,\n agent_working_dir,\n setup_completed_at AS \"setup_completed_at: DateTime<Utc>\",\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n archived AS \"archived!: bool\",\n pinned AS \"pinned!: bool\",\n name\n FROM workspaces\n WHERE task_id = $1\n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,21 @@
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "archived!: bool",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "pinned!: bool",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +70,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4f2044112bb23e40b229804cfa535a6b631d95ff7b5596706711f76a50516b40"
|
||||
"hash": "c4619cdaa3e646384c40aa5e73571b7012d15d53d9e3c96bf154259e369448d6"
|
||||
}
|
||||
20
crates/db/.sqlx/query-d9f7205a1a749c23c928ef2861783446bd99a7b7939929dee1f8a409bb99ab04.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT DISTINCT s.workspace_id as \"workspace_id!: Uuid\"\n FROM execution_processes ep\n JOIN sessions s ON ep.session_id = s.id\n JOIN workspaces w ON s.workspace_id = w.id\n WHERE w.archived = $1\n AND ep.status = 'running'\n AND ep.run_reason = 'devserver'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d9f7205a1a749c23c928ef2861783446bd99a7b7939929dee1f8a409bb99ab04"
|
||||
}
|
||||
11
crates/db/migrations/20251221000000_add_workspace_flags.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Add workspace flags for archived, pinned, and name
|
||||
ALTER TABLE workspaces ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE workspaces ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE workspaces ADD COLUMN name TEXT;
|
||||
|
||||
-- Archive workspaces for completed/cancelled tasks
|
||||
UPDATE workspaces
|
||||
SET archived = 1
|
||||
WHERE task_id IN (
|
||||
SELECT id FROM tasks WHERE status IN ('done', 'cancelled')
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add 'seen' column to coding_agent_turns table
|
||||
-- New turns default to unseen (0), marked as seen (1) when user views the workspace
|
||||
ALTER TABLE coding_agent_turns ADD COLUMN seen INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -11,6 +11,7 @@ pub struct CodingAgentTurn {
|
||||
pub agent_session_id: Option<String>, // Session ID from Claude/Amp coding agent
|
||||
pub prompt: Option<String>, // The prompt sent to the executor
|
||||
pub summary: Option<String>, // Final assistant message/summary
|
||||
pub seen: bool, // Whether user has viewed this turn
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -35,6 +36,7 @@ impl CodingAgentTurn {
|
||||
agent_session_id,
|
||||
prompt,
|
||||
summary,
|
||||
seen as "seen!: bool",
|
||||
created_at as "created_at!: DateTime<Utc>",
|
||||
updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM coding_agent_turns
|
||||
@@ -57,6 +59,7 @@ impl CodingAgentTurn {
|
||||
agent_session_id,
|
||||
prompt,
|
||||
summary,
|
||||
seen as "seen!: bool",
|
||||
created_at as "created_at!: DateTime<Utc>",
|
||||
updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM coding_agent_turns
|
||||
@@ -86,16 +89,17 @@ impl CodingAgentTurn {
|
||||
sqlx::query_as!(
|
||||
CodingAgentTurn,
|
||||
r#"INSERT INTO coding_agent_turns (
|
||||
id, execution_process_id, agent_session_id, prompt, summary,
|
||||
id, execution_process_id, agent_session_id, prompt, summary, seen,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING
|
||||
id as "id!: Uuid",
|
||||
execution_process_id as "execution_process_id!: Uuid",
|
||||
agent_session_id,
|
||||
prompt,
|
||||
summary,
|
||||
seen as "seen!: bool",
|
||||
created_at as "created_at!: DateTime<Utc>",
|
||||
updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
id,
|
||||
@@ -103,6 +107,7 @@ impl CodingAgentTurn {
|
||||
None::<String>, // agent_session_id initially None until parsed from output
|
||||
data.prompt,
|
||||
None::<String>, // summary initially None
|
||||
false, // seen - defaults to unseen
|
||||
now, // created_at
|
||||
now // updated_at
|
||||
)
|
||||
@@ -151,4 +156,67 @@ impl CodingAgentTurn {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark all coding agent turns for a workspace as seen
|
||||
pub async fn mark_seen_by_workspace_id(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let now = Utc::now();
|
||||
sqlx::query!(
|
||||
r#"UPDATE coding_agent_turns
|
||||
SET seen = 1, updated_at = $1
|
||||
WHERE execution_process_id IN (
|
||||
SELECT ep.id FROM execution_processes ep
|
||||
JOIN sessions s ON ep.session_id = s.id
|
||||
WHERE s.workspace_id = $2
|
||||
) AND seen = 0"#,
|
||||
now,
|
||||
workspace_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a workspace has any unseen coding agent turns
|
||||
pub async fn has_unseen_by_workspace_id(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
let result = sqlx::query_scalar!(
|
||||
r#"SELECT EXISTS(
|
||||
SELECT 1 FROM coding_agent_turns cat
|
||||
JOIN execution_processes ep ON cat.execution_process_id = ep.id
|
||||
JOIN sessions s ON ep.session_id = s.id
|
||||
WHERE s.workspace_id = $1 AND cat.seen = 0
|
||||
) as "has_unseen!: bool""#,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Find all workspaces that have unseen coding agent turns, filtered by archived status
|
||||
pub async fn find_workspaces_with_unseen(
|
||||
pool: &SqlitePool,
|
||||
archived: bool,
|
||||
) -> Result<std::collections::HashSet<Uuid>, sqlx::Error> {
|
||||
let result: Vec<Uuid> = sqlx::query_scalar!(
|
||||
r#"SELECT DISTINCT s.workspace_id as "workspace_id!: Uuid"
|
||||
FROM coding_agent_turns cat
|
||||
JOIN execution_processes ep ON cat.execution_process_id = ep.id
|
||||
JOIN sessions s ON ep.session_id = s.id
|
||||
JOIN workspaces w ON s.workspace_id = w.id
|
||||
WHERE cat.seen = 0 AND w.archived = $1"#,
|
||||
archived
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use executors::{
|
||||
actions::{ExecutorAction, ExecutorActionType},
|
||||
@@ -101,6 +103,16 @@ pub struct ExecutionContext {
|
||||
pub repos: Vec<Repo>,
|
||||
}
|
||||
|
||||
/// Summary info about the latest execution process for a workspace
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct LatestProcessInfo {
|
||||
pub workspace_id: Uuid,
|
||||
pub execution_process_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub status: ExecutionProcessStatus,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ExecutorActionField {
|
||||
@@ -629,11 +641,12 @@ impl ExecutionProcess {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the latest CodingAgent executor profile for a session
|
||||
/// Fetch the latest CodingAgent executor profile for a session.
|
||||
/// Returns None if no CodingAgent execution process exists for this session.
|
||||
pub async fn latest_executor_profile_for_session(
|
||||
pool: &SqlitePool,
|
||||
session_id: Uuid,
|
||||
) -> Result<ExecutorProfileId, ExecutionProcessError> {
|
||||
) -> Result<Option<ExecutorProfileId>, ExecutionProcessError> {
|
||||
// Find the latest CodingAgent execution process for this session
|
||||
let latest_execution_process = sqlx::query_as!(
|
||||
ExecutionProcess,
|
||||
@@ -656,12 +669,11 @@ impl ExecutionProcess {
|
||||
ExecutionProcessRunReason::CodingAgent
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ExecutionProcessError::ValidationError(
|
||||
"Couldn't find initial coding agent process, has it run yet?".to_string(),
|
||||
)
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let Some(latest_execution_process) = latest_execution_process else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let action = latest_execution_process
|
||||
.executor_action()
|
||||
@@ -669,14 +681,82 @@ impl ExecutionProcess {
|
||||
|
||||
match &action.typ {
|
||||
ExecutorActionType::CodingAgentInitialRequest(request) => {
|
||||
Ok(request.executor_profile_id.clone())
|
||||
Ok(Some(request.executor_profile_id.clone()))
|
||||
}
|
||||
ExecutorActionType::CodingAgentFollowUpRequest(request) => {
|
||||
Ok(request.executor_profile_id.clone())
|
||||
Ok(Some(request.executor_profile_id.clone()))
|
||||
}
|
||||
_ => Err(ExecutionProcessError::ValidationError(
|
||||
"Couldn't find profile from initial request".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch latest execution process info for all workspaces with the given archived status.
|
||||
/// Returns a map of workspace_id -> LatestProcessInfo for the most recent
|
||||
/// non-dropped execution process (excluding dev servers).
|
||||
pub async fn find_latest_for_workspaces(
|
||||
pool: &SqlitePool,
|
||||
archived: bool,
|
||||
) -> Result<HashMap<Uuid, LatestProcessInfo>, sqlx::Error> {
|
||||
let rows: Vec<LatestProcessInfo> = sqlx::query_as!(
|
||||
LatestProcessInfo,
|
||||
r#"
|
||||
SELECT
|
||||
s.workspace_id as "workspace_id!: Uuid",
|
||||
ep.id as "execution_process_id!: Uuid",
|
||||
ep.session_id as "session_id!: Uuid",
|
||||
ep.status as "status!: ExecutionProcessStatus",
|
||||
ep.completed_at as "completed_at?: DateTime<Utc>"
|
||||
FROM execution_processes ep
|
||||
JOIN sessions s ON ep.session_id = s.id
|
||||
JOIN workspaces w ON s.workspace_id = w.id
|
||||
WHERE w.archived = $1
|
||||
AND ep.run_reason IN ('codingagent', 'setupscript', 'cleanupscript')
|
||||
AND ep.dropped = FALSE
|
||||
AND ep.created_at = (
|
||||
SELECT MAX(ep2.created_at)
|
||||
FROM execution_processes ep2
|
||||
JOIN sessions s2 ON ep2.session_id = s2.id
|
||||
WHERE s2.workspace_id = s.workspace_id
|
||||
AND ep2.run_reason IN ('codingagent', 'setupscript', 'cleanupscript')
|
||||
AND ep2.dropped = FALSE
|
||||
)
|
||||
"#,
|
||||
archived
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|info| (info.workspace_id, info))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Find all workspaces with running dev servers, filtered by archived status.
|
||||
/// Returns a set of workspace IDs that have at least one running dev server.
|
||||
pub async fn find_workspaces_with_running_dev_servers(
|
||||
pool: &SqlitePool,
|
||||
archived: bool,
|
||||
) -> Result<HashSet<Uuid>, sqlx::Error> {
|
||||
let rows: Vec<Uuid> = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT DISTINCT s.workspace_id as "workspace_id!: Uuid"
|
||||
FROM execution_processes ep
|
||||
JOIN sessions s ON ep.session_id = s.id
|
||||
JOIN workspaces w ON s.workspace_id = w.id
|
||||
WHERE w.archived = $1
|
||||
AND ep.status = 'running'
|
||||
AND ep.run_reason = 'devserver'
|
||||
"#,
|
||||
archived
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ pub struct SearchResult {
|
||||
pub path: String,
|
||||
pub is_file: bool,
|
||||
pub match_type: SearchMatchType,
|
||||
/// Ranking score based on git history (higher = more recently/frequently edited)
|
||||
#[serde(default)]
|
||||
pub score: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, TS)]
|
||||
|
||||
@@ -80,6 +80,21 @@ impl Repo {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_ids(pool: &SqlitePool, ids: &[Uuid]) -> Result<Vec<Self>, sqlx::Error> {
|
||||
if ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Fetch each repo individually since SQLite doesn't support array parameters
|
||||
let mut repos = Vec::with_capacity(ids.len());
|
||||
for id in ids {
|
||||
if let Some(repo) = Self::find_by_id(pool, *id).await? {
|
||||
repos.push(repo);
|
||||
}
|
||||
}
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
pub async fn find_or_create<'e, E>(
|
||||
executor: E,
|
||||
path: &Path,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use executors::profile::ExecutorProfileId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use strum_macros::{Display, EnumDiscriminants, EnumString};
|
||||
@@ -24,6 +25,25 @@ pub struct DraftFollowUpData {
|
||||
pub variant: Option<String>,
|
||||
}
|
||||
|
||||
/// Data for a draft workspace scratch (new workspace creation)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
pub struct DraftWorkspaceData {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub project_id: Option<Uuid>,
|
||||
#[serde(default)]
|
||||
pub repos: Vec<DraftWorkspaceRepo>,
|
||||
#[serde(default)]
|
||||
pub selected_profile: Option<ExecutorProfileId>,
|
||||
}
|
||||
|
||||
/// Repository entry in a draft workspace
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
pub struct DraftWorkspaceRepo {
|
||||
pub repo_id: Uuid,
|
||||
pub target_branch: String,
|
||||
}
|
||||
|
||||
/// The payload of a scratch, tagged by type. The type is part of the composite primary key.
|
||||
/// Data is stored as markdown string.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, EnumDiscriminants)]
|
||||
@@ -36,6 +56,7 @@ pub struct DraftFollowUpData {
|
||||
pub enum ScratchPayload {
|
||||
DraftTask(String),
|
||||
DraftFollowUp(DraftFollowUpData),
|
||||
DraftWorkspace(DraftWorkspaceData),
|
||||
}
|
||||
|
||||
impl ScratchPayload {
|
||||
|
||||
@@ -46,41 +46,58 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find all sessions for a workspace, ordered by most recently used.
|
||||
/// "Most recently used" is defined as the most recent non-dev server execution process.
|
||||
/// Sessions with no executions fall back to created_at for ordering.
|
||||
pub async fn find_by_workspace_id(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Session,
|
||||
r#"SELECT id AS "id!: Uuid",
|
||||
workspace_id AS "workspace_id!: Uuid",
|
||||
executor,
|
||||
created_at AS "created_at!: DateTime<Utc>",
|
||||
updated_at AS "updated_at!: DateTime<Utc>"
|
||||
FROM sessions
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC"#,
|
||||
r#"SELECT s.id AS "id!: Uuid",
|
||||
s.workspace_id AS "workspace_id!: Uuid",
|
||||
s.executor,
|
||||
s.created_at AS "created_at!: DateTime<Utc>",
|
||||
s.updated_at AS "updated_at!: DateTime<Utc>"
|
||||
FROM sessions s
|
||||
LEFT JOIN (
|
||||
SELECT ep.session_id, MAX(ep.created_at) as last_used
|
||||
FROM execution_processes ep
|
||||
WHERE ep.run_reason != 'devserver' AND ep.dropped = FALSE
|
||||
GROUP BY ep.session_id
|
||||
) latest_ep ON s.id = latest_ep.session_id
|
||||
WHERE s.workspace_id = $1
|
||||
ORDER BY COALESCE(latest_ep.last_used, s.created_at) DESC"#,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find the latest session for a workspace
|
||||
/// Find the most recently used session for a workspace.
|
||||
/// "Most recently used" is defined as the most recent non-dev server execution process.
|
||||
/// Sessions with no executions fall back to created_at for ordering.
|
||||
pub async fn find_latest_by_workspace_id(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Session,
|
||||
r#"SELECT id AS "id!: Uuid",
|
||||
workspace_id AS "workspace_id!: Uuid",
|
||||
executor,
|
||||
created_at AS "created_at!: DateTime<Utc>",
|
||||
updated_at AS "updated_at!: DateTime<Utc>"
|
||||
FROM sessions
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
r#"SELECT s.id AS "id!: Uuid",
|
||||
s.workspace_id AS "workspace_id!: Uuid",
|
||||
s.executor,
|
||||
s.created_at AS "created_at!: DateTime<Utc>",
|
||||
s.updated_at AS "updated_at!: DateTime<Utc>"
|
||||
FROM sessions s
|
||||
LEFT JOIN (
|
||||
SELECT ep.session_id, MAX(ep.created_at) as last_used
|
||||
FROM execution_processes ep
|
||||
WHERE ep.run_reason != 'devserver' AND ep.dropped = FALSE
|
||||
GROUP BY ep.session_id
|
||||
) latest_ep ON s.id = latest_ep.session_id
|
||||
WHERE s.workspace_id = $1
|
||||
ORDER BY COALESCE(latest_ep.last_used, s.created_at) DESC
|
||||
LIMIT 1"#,
|
||||
workspace_id
|
||||
)
|
||||
|
||||
@@ -54,6 +54,25 @@ pub struct Workspace {
|
||||
pub setup_completed_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub archived: bool,
|
||||
pub pinned: bool,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
pub struct WorkspaceWithStatus {
|
||||
#[serde(flatten)]
|
||||
#[ts(flatten)]
|
||||
pub workspace: Workspace,
|
||||
pub is_running: bool,
|
||||
pub is_errored: bool,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for WorkspaceWithStatus {
|
||||
type Target = Workspace;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.workspace
|
||||
}
|
||||
}
|
||||
|
||||
/// GitHub PR creation parameters
|
||||
@@ -113,7 +132,10 @@ impl Workspace {
|
||||
agent_working_dir,
|
||||
setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
created_at AS "created_at!: DateTime<Utc>",
|
||||
updated_at AS "updated_at!: DateTime<Utc>"
|
||||
updated_at AS "updated_at!: DateTime<Utc>",
|
||||
archived AS "archived!: bool",
|
||||
pinned AS "pinned!: bool",
|
||||
name
|
||||
FROM workspaces
|
||||
WHERE task_id = $1
|
||||
ORDER BY created_at DESC"#,
|
||||
@@ -131,7 +153,10 @@ impl Workspace {
|
||||
agent_working_dir,
|
||||
setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
created_at AS "created_at!: DateTime<Utc>",
|
||||
updated_at AS "updated_at!: DateTime<Utc>"
|
||||
updated_at AS "updated_at!: DateTime<Utc>",
|
||||
archived AS "archived!: bool",
|
||||
pinned AS "pinned!: bool",
|
||||
name
|
||||
FROM workspaces
|
||||
ORDER BY created_at DESC"#
|
||||
)
|
||||
@@ -159,7 +184,10 @@ impl Workspace {
|
||||
w.agent_working_dir,
|
||||
w.setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
w.created_at AS "created_at!: DateTime<Utc>",
|
||||
w.updated_at AS "updated_at!: DateTime<Utc>"
|
||||
w.updated_at AS "updated_at!: DateTime<Utc>",
|
||||
w.archived AS "archived!: bool",
|
||||
w.pinned AS "pinned!: bool",
|
||||
w.name
|
||||
FROM workspaces w
|
||||
JOIN tasks t ON w.task_id = t.id
|
||||
JOIN projects p ON t.project_id = p.id
|
||||
@@ -245,7 +273,10 @@ impl Workspace {
|
||||
agent_working_dir,
|
||||
setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
created_at AS "created_at!: DateTime<Utc>",
|
||||
updated_at AS "updated_at!: DateTime<Utc>"
|
||||
updated_at AS "updated_at!: DateTime<Utc>",
|
||||
archived AS "archived!: bool",
|
||||
pinned AS "pinned!: bool",
|
||||
name
|
||||
FROM workspaces
|
||||
WHERE id = $1"#,
|
||||
id
|
||||
@@ -264,7 +295,10 @@ impl Workspace {
|
||||
agent_working_dir,
|
||||
setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
created_at AS "created_at!: DateTime<Utc>",
|
||||
updated_at AS "updated_at!: DateTime<Utc>"
|
||||
updated_at AS "updated_at!: DateTime<Utc>",
|
||||
archived AS "archived!: bool",
|
||||
pinned AS "pinned!: bool",
|
||||
name
|
||||
FROM workspaces
|
||||
WHERE rowid = $1"#,
|
||||
rowid
|
||||
@@ -302,7 +336,10 @@ impl Workspace {
|
||||
w.agent_working_dir,
|
||||
w.setup_completed_at as "setup_completed_at: DateTime<Utc>",
|
||||
w.created_at as "created_at!: DateTime<Utc>",
|
||||
w.updated_at as "updated_at!: DateTime<Utc>"
|
||||
w.updated_at as "updated_at!: DateTime<Utc>",
|
||||
w.archived as "archived!: bool",
|
||||
w.pinned as "pinned!: bool",
|
||||
w.name
|
||||
FROM workspaces w
|
||||
LEFT JOIN sessions s ON w.id = s.workspace_id
|
||||
LEFT JOIN execution_processes ep ON s.id = ep.session_id AND ep.completed_at IS NOT NULL
|
||||
@@ -344,7 +381,7 @@ impl Workspace {
|
||||
Workspace,
|
||||
r#"INSERT INTO workspaces (id, task_id, container_ref, branch, agent_working_dir, setup_completed_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", container_ref, branch, agent_working_dir, setup_completed_at as "setup_completed_at: DateTime<Utc>", created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", container_ref, branch, agent_working_dir, setup_completed_at as "setup_completed_at: DateTime<Utc>", created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>", archived as "archived!: bool", pinned as "pinned!: bool", name"#,
|
||||
id,
|
||||
task_id,
|
||||
Option::<String>::None,
|
||||
@@ -395,4 +432,258 @@ impl Workspace {
|
||||
project_id: result.project_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_archived(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
archived: bool,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE workspaces SET archived = $1, updated_at = datetime('now', 'subsec') WHERE id = $2",
|
||||
archived,
|
||||
workspace_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update workspace fields. Only non-None values will be updated.
|
||||
/// For `name`, pass `Some("")` to clear the name, `Some("foo")` to set it, or `None` to leave unchanged.
|
||||
pub async fn update(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
archived: Option<bool>,
|
||||
pinned: Option<bool>,
|
||||
name: Option<&str>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
// Convert empty string to None for name field (to store as NULL)
|
||||
let name_value = name.filter(|s| !s.is_empty());
|
||||
let name_provided = name.is_some();
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE workspaces SET
|
||||
archived = COALESCE($1, archived),
|
||||
pinned = COALESCE($2, pinned),
|
||||
name = CASE WHEN $3 THEN $4 ELSE name END,
|
||||
updated_at = datetime('now', 'subsec')
|
||||
WHERE id = $5"#,
|
||||
archived,
|
||||
pinned,
|
||||
name_provided,
|
||||
name_value,
|
||||
workspace_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_first_user_message(
|
||||
pool: &SqlitePool,
|
||||
workspace_id: Uuid,
|
||||
) -> Result<Option<String>, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"SELECT cat.prompt
|
||||
FROM sessions s
|
||||
JOIN execution_processes ep ON ep.session_id = s.id
|
||||
JOIN coding_agent_turns cat ON cat.execution_process_id = ep.id
|
||||
WHERE s.workspace_id = $1
|
||||
AND s.executor IS NOT NULL
|
||||
AND cat.prompt IS NOT NULL
|
||||
ORDER BY s.created_at ASC, ep.created_at ASC
|
||||
LIMIT 1"#,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(result.and_then(|r| r.prompt))
|
||||
}
|
||||
|
||||
pub fn truncate_to_name(prompt: &str, max_len: usize) -> String {
|
||||
let trimmed = prompt.trim();
|
||||
if trimmed.chars().count() <= max_len {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
let truncated: String = trimmed.chars().take(max_len).collect();
|
||||
if let Some(last_space) = truncated.rfind(' ') {
|
||||
format!("{}...", &truncated[..last_space])
|
||||
} else {
|
||||
format!("{}...", truncated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_all_with_status(
|
||||
pool: &SqlitePool,
|
||||
archived: Option<bool>,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<WorkspaceWithStatus>, sqlx::Error> {
|
||||
// Fetch all workspaces with status (uses cached SQLx query)
|
||||
let records = sqlx::query!(
|
||||
r#"SELECT
|
||||
w.id AS "id!: Uuid",
|
||||
w.task_id AS "task_id!: Uuid",
|
||||
w.container_ref,
|
||||
w.branch,
|
||||
w.agent_working_dir,
|
||||
w.setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
w.created_at AS "created_at!: DateTime<Utc>",
|
||||
w.updated_at AS "updated_at!: DateTime<Utc>",
|
||||
w.archived AS "archived!: bool",
|
||||
w.pinned AS "pinned!: bool",
|
||||
w.name,
|
||||
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM sessions s
|
||||
JOIN execution_processes ep ON ep.session_id = s.id
|
||||
WHERE s.workspace_id = w.id
|
||||
AND ep.status = 'running'
|
||||
AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')
|
||||
LIMIT 1
|
||||
) THEN 1 ELSE 0 END AS "is_running!: i64",
|
||||
|
||||
CASE WHEN (
|
||||
SELECT ep.status
|
||||
FROM sessions s
|
||||
JOIN execution_processes ep ON ep.session_id = s.id
|
||||
WHERE s.workspace_id = w.id
|
||||
AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')
|
||||
ORDER BY ep.created_at DESC
|
||||
LIMIT 1
|
||||
) IN ('failed','killed') THEN 1 ELSE 0 END AS "is_errored!: i64"
|
||||
|
||||
FROM workspaces w
|
||||
ORDER BY w.updated_at DESC"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut workspaces: Vec<WorkspaceWithStatus> = records
|
||||
.into_iter()
|
||||
.map(|rec| WorkspaceWithStatus {
|
||||
workspace: Workspace {
|
||||
id: rec.id,
|
||||
task_id: rec.task_id,
|
||||
container_ref: rec.container_ref,
|
||||
branch: rec.branch,
|
||||
agent_working_dir: rec.agent_working_dir,
|
||||
setup_completed_at: rec.setup_completed_at,
|
||||
created_at: rec.created_at,
|
||||
updated_at: rec.updated_at,
|
||||
archived: rec.archived,
|
||||
pinned: rec.pinned,
|
||||
name: rec.name,
|
||||
},
|
||||
is_running: rec.is_running != 0,
|
||||
is_errored: rec.is_errored != 0,
|
||||
})
|
||||
// Apply archived filter if provided
|
||||
.filter(|ws| archived.is_none_or(|a| ws.workspace.archived == a))
|
||||
.collect();
|
||||
|
||||
// Apply limit if provided (already sorted by updated_at DESC from query)
|
||||
if let Some(lim) = limit {
|
||||
workspaces.truncate(lim as usize);
|
||||
}
|
||||
|
||||
for ws in &mut workspaces {
|
||||
if ws.workspace.name.is_none()
|
||||
&& let Some(prompt) = Self::get_first_user_message(pool, ws.workspace.id).await?
|
||||
{
|
||||
let name = Self::truncate_to_name(&prompt, 35);
|
||||
Self::update(pool, ws.workspace.id, None, None, Some(&name)).await?;
|
||||
ws.workspace.name = Some(name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
/// Delete a workspace by ID
|
||||
pub async fn delete(pool: &SqlitePool, id: Uuid) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query!("DELETE FROM workspaces WHERE id = $1", id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
pub async fn find_by_id_with_status(
|
||||
pool: &SqlitePool,
|
||||
id: Uuid,
|
||||
) -> Result<Option<WorkspaceWithStatus>, sqlx::Error> {
|
||||
let rec = sqlx::query!(
|
||||
r#"SELECT
|
||||
w.id AS "id!: Uuid",
|
||||
w.task_id AS "task_id!: Uuid",
|
||||
w.container_ref,
|
||||
w.branch,
|
||||
w.agent_working_dir,
|
||||
w.setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
||||
w.created_at AS "created_at!: DateTime<Utc>",
|
||||
w.updated_at AS "updated_at!: DateTime<Utc>",
|
||||
w.archived AS "archived!: bool",
|
||||
w.pinned AS "pinned!: bool",
|
||||
w.name,
|
||||
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM sessions s
|
||||
JOIN execution_processes ep ON ep.session_id = s.id
|
||||
WHERE s.workspace_id = w.id
|
||||
AND ep.status = 'running'
|
||||
AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')
|
||||
LIMIT 1
|
||||
) THEN 1 ELSE 0 END AS "is_running!: i64",
|
||||
|
||||
CASE WHEN (
|
||||
SELECT ep.status
|
||||
FROM sessions s
|
||||
JOIN execution_processes ep ON ep.session_id = s.id
|
||||
WHERE s.workspace_id = w.id
|
||||
AND ep.run_reason IN ('setupscript','cleanupscript','codingagent')
|
||||
ORDER BY ep.created_at DESC
|
||||
LIMIT 1
|
||||
) IN ('failed','killed') THEN 1 ELSE 0 END AS "is_errored!: i64"
|
||||
|
||||
FROM workspaces w
|
||||
WHERE w.id = $1"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
let Some(rec) = rec else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut ws = WorkspaceWithStatus {
|
||||
workspace: Workspace {
|
||||
id: rec.id,
|
||||
task_id: rec.task_id,
|
||||
container_ref: rec.container_ref,
|
||||
branch: rec.branch,
|
||||
agent_working_dir: rec.agent_working_dir,
|
||||
setup_completed_at: rec.setup_completed_at,
|
||||
created_at: rec.created_at,
|
||||
updated_at: rec.updated_at,
|
||||
archived: rec.archived,
|
||||
pinned: rec.pinned,
|
||||
name: rec.name,
|
||||
},
|
||||
is_running: rec.is_running != 0,
|
||||
is_errored: rec.is_errored != 0,
|
||||
};
|
||||
|
||||
if ws.workspace.name.is_none()
|
||||
&& let Some(prompt) = Self::get_first_user_message(pool, ws.workspace.id).await?
|
||||
{
|
||||
let name = Self::truncate_to_name(&prompt, 35);
|
||||
Self::update(pool, ws.workspace.id, None, None, Some(&name)).await?;
|
||||
ws.workspace.name = Some(name);
|
||||
}
|
||||
|
||||
Ok(Some(ws))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,10 @@ impl ClaudeLogProcessor {
|
||||
while let Some(Ok(msg)) = stream.next().await {
|
||||
let chunk = match msg {
|
||||
LogMsg::Stdout(x) => x,
|
||||
LogMsg::JsonPatch(_) | LogMsg::SessionId(_) | LogMsg::Stderr(_) => continue,
|
||||
LogMsg::JsonPatch(_)
|
||||
| LogMsg::SessionId(_)
|
||||
| LogMsg::Stderr(_)
|
||||
| LogMsg::Ready => continue,
|
||||
LogMsg::Finished => break,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -792,16 +793,31 @@ impl LocalContainerService {
|
||||
ctx: &ExecutionContext,
|
||||
queued_data: &DraftFollowUpData,
|
||||
) -> Result<ExecutionProcess, ContainerError> {
|
||||
// Get executor profile from the latest CodingAgent process in this session
|
||||
let initial_executor_profile_id =
|
||||
ExecutionProcess::latest_executor_profile_for_session(&self.db.pool, ctx.session.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ContainerError::Other(anyhow!("Failed to get executor profile: {e}"))
|
||||
// Get executor from the latest CodingAgent process, or fall back to session's executor
|
||||
let base_executor = match ExecutionProcess::latest_executor_profile_for_session(
|
||||
&self.db.pool,
|
||||
ctx.session.id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ContainerError::Other(anyhow!("Failed to get executor profile: {e}")))?
|
||||
{
|
||||
Some(profile) => profile.executor,
|
||||
None => {
|
||||
// No prior execution - use session's executor field
|
||||
let executor_str = ctx.session.executor.as_ref().ok_or_else(|| {
|
||||
ContainerError::Other(anyhow!(
|
||||
"No prior execution and no executor configured on session"
|
||||
))
|
||||
})?;
|
||||
BaseCodingAgent::from_str(&executor_str.replace('-', "_").to_ascii_uppercase())
|
||||
.map_err(|_| {
|
||||
ContainerError::Other(anyhow!("Invalid executor: {}", executor_str))
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
let executor_profile_id = ExecutorProfileId {
|
||||
executor: initial_executor_profile_id.executor,
|
||||
executor: base_executor,
|
||||
variant: queued_data.variant.clone(),
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ fn generate_types_content() -> String {
|
||||
db::models::task::CreateTask::decl(),
|
||||
db::models::task::UpdateTask::decl(),
|
||||
db::models::scratch::DraftFollowUpData::decl(),
|
||||
db::models::scratch::DraftWorkspaceData::decl(),
|
||||
db::models::scratch::DraftWorkspaceRepo::decl(),
|
||||
db::models::scratch::ScratchPayload::decl(),
|
||||
db::models::scratch::ScratchType::decl(),
|
||||
db::models::scratch::Scratch::decl(),
|
||||
@@ -45,6 +47,7 @@ fn generate_types_content() -> String {
|
||||
db::models::image::Image::decl(),
|
||||
db::models::image::CreateImage::decl(),
|
||||
db::models::workspace::Workspace::decl(),
|
||||
db::models::workspace::WorkspaceWithStatus::decl(),
|
||||
db::models::session::Session::decl(),
|
||||
db::models::execution_process::ExecutionProcess::decl(),
|
||||
db::models::execution_process::ExecutionProcessStatus::decl(),
|
||||
@@ -131,6 +134,7 @@ fn generate_types_content() -> String {
|
||||
server::routes::task_attempts::pr::CreatePrError::decl(),
|
||||
server::routes::task_attempts::BranchStatus::decl(),
|
||||
server::routes::task_attempts::RunScriptError::decl(),
|
||||
server::routes::task_attempts::DeleteWorkspaceError::decl(),
|
||||
server::routes::task_attempts::pr::AttachPrResponse::decl(),
|
||||
server::routes::task_attempts::pr::AttachExistingPrRequest::decl(),
|
||||
server::routes::task_attempts::pr::PrCommentsResponse::decl(),
|
||||
@@ -138,6 +142,10 @@ fn generate_types_content() -> String {
|
||||
server::routes::task_attempts::pr::GetPrCommentsQuery::decl(),
|
||||
services::services::github::UnifiedPrComment::decl(),
|
||||
server::routes::task_attempts::RepoBranchStatus::decl(),
|
||||
server::routes::task_attempts::UpdateWorkspace::decl(),
|
||||
server::routes::task_attempts::workspace_summary::WorkspaceSummaryRequest::decl(),
|
||||
server::routes::task_attempts::workspace_summary::WorkspaceSummary::decl(),
|
||||
server::routes::task_attempts::workspace_summary::WorkspaceSummaryResponse::decl(),
|
||||
services::services::filesystem::DirectoryEntry::decl(),
|
||||
services::services::filesystem::DirectoryListResponse::decl(),
|
||||
services::services::config::Config::decl(),
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Json as ResponseJson,
|
||||
routing::post,
|
||||
};
|
||||
use deployment::Deployment;
|
||||
use utils::approvals::{ApprovalResponse, ApprovalStatus};
|
||||
use utils::{
|
||||
approvals::{ApprovalResponse, ApprovalStatus},
|
||||
response::ApiResponse,
|
||||
};
|
||||
|
||||
use crate::DeploymentImpl;
|
||||
|
||||
pub async fn respond_to_approval(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Path(id): Path<String>,
|
||||
Json(request): Json<ApprovalResponse>,
|
||||
) -> Result<Json<ApprovalStatus>, StatusCode> {
|
||||
ResponseJson(request): ResponseJson<ApprovalResponse>,
|
||||
) -> Result<ResponseJson<ApiResponse<ApprovalStatus>>, StatusCode> {
|
||||
let service = deployment.approvals();
|
||||
|
||||
match service.respond(&deployment.db().pool, &id, request).await {
|
||||
@@ -30,7 +34,7 @@ pub async fn respond_to_approval(
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(status))
|
||||
Ok(ResponseJson(ApiResponse::success(status)))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to respond to approval: {:?}", e);
|
||||
|
||||
@@ -23,8 +23,8 @@ use uuid::Uuid;
|
||||
use crate::{DeploymentImpl, error::ApiError, middleware::load_execution_process_middleware};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ExecutionProcessQuery {
|
||||
pub workspace_id: Uuid,
|
||||
pub struct SessionExecutionProcessQuery {
|
||||
pub session_id: Uuid,
|
||||
/// If true, include soft-deleted (dropped) processes in results/stream
|
||||
#[serde(default)]
|
||||
pub show_soft_deleted: Option<bool>,
|
||||
@@ -178,35 +178,35 @@ pub async fn stop_execution_process(
|
||||
Ok(ResponseJson(ApiResponse::success(())))
|
||||
}
|
||||
|
||||
pub async fn stream_execution_processes_ws(
|
||||
pub async fn stream_execution_processes_by_session_ws(
|
||||
ws: WebSocketUpgrade,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Query(query): Query<ExecutionProcessQuery>,
|
||||
Query(query): Query<SessionExecutionProcessQuery>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
if let Err(e) = handle_execution_processes_ws(
|
||||
if let Err(e) = handle_execution_processes_by_session_ws(
|
||||
socket,
|
||||
deployment,
|
||||
query.workspace_id,
|
||||
query.session_id,
|
||||
query.show_soft_deleted.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("execution processes WS closed: {}", e);
|
||||
tracing::warn!("execution processes by session WS closed: {}", e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_execution_processes_ws(
|
||||
async fn handle_execution_processes_by_session_ws(
|
||||
socket: WebSocket,
|
||||
deployment: DeploymentImpl,
|
||||
workspace_id: uuid::Uuid,
|
||||
session_id: uuid::Uuid,
|
||||
show_soft_deleted: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
// Get the raw stream and convert LogMsg to WebSocket messages
|
||||
let mut stream = deployment
|
||||
.events()
|
||||
.stream_execution_processes_for_workspace_raw(workspace_id, show_soft_deleted)
|
||||
.stream_execution_processes_for_session_raw(session_id, show_soft_deleted)
|
||||
.await?
|
||||
.map_ok(|msg| msg.to_ws_message_unchecked());
|
||||
|
||||
@@ -256,7 +256,10 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
));
|
||||
|
||||
let workspaces_router = Router::new()
|
||||
.route("/stream/ws", get(stream_execution_processes_ws))
|
||||
.route(
|
||||
"/stream/session/ws",
|
||||
get(stream_execution_processes_by_session_ws),
|
||||
)
|
||||
.nest("/{id}", workspace_id_router);
|
||||
|
||||
Router::new().nest("/execution-processes", workspaces_router)
|
||||
|
||||
@@ -313,8 +313,8 @@ pub async fn delete_project(
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct OpenEditorRequest {
|
||||
editor_type: Option<String>,
|
||||
git_repo_path: Option<PathBuf>,
|
||||
pub editor_type: Option<String>,
|
||||
pub git_repo_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, ts_rs::TS)]
|
||||
|
||||
@@ -12,7 +12,11 @@ use ts_rs::TS;
|
||||
use utils::response::ApiResponse;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{DeploymentImpl, error::ApiError};
|
||||
use crate::{
|
||||
DeploymentImpl,
|
||||
error::ApiError,
|
||||
routes::projects::{OpenEditorRequest, OpenEditorResponse},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
@@ -28,6 +32,12 @@ pub struct InitRepoRequest {
|
||||
pub folder_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct BatchRepoRequest {
|
||||
pub ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
pub async fn register_repo(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
ResponseJson(payload): ResponseJson<RegisterRepoRequest>,
|
||||
@@ -74,9 +84,66 @@ pub async fn get_repo_branches(
|
||||
Ok(ResponseJson(ApiResponse::success(branches)))
|
||||
}
|
||||
|
||||
pub async fn get_repos_batch(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
ResponseJson(payload): ResponseJson<BatchRepoRequest>,
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<Repo>>>, ApiError> {
|
||||
let repos = Repo::find_by_ids(&deployment.db().pool, &payload.ids).await?;
|
||||
Ok(ResponseJson(ApiResponse::success(repos)))
|
||||
}
|
||||
|
||||
pub async fn open_repo_in_editor(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Path(repo_id): Path<Uuid>,
|
||||
ResponseJson(payload): ResponseJson<Option<OpenEditorRequest>>,
|
||||
) -> Result<ResponseJson<ApiResponse<OpenEditorResponse>>, ApiError> {
|
||||
let repo = deployment
|
||||
.repo()
|
||||
.get_by_id(&deployment.db().pool, repo_id)
|
||||
.await?;
|
||||
|
||||
let editor_config = {
|
||||
let config = deployment.config().read().await;
|
||||
let editor_type_str = payload.as_ref().and_then(|req| req.editor_type.as_deref());
|
||||
config.editor.with_override(editor_type_str)
|
||||
};
|
||||
|
||||
match editor_config.open_file(&repo.path).await {
|
||||
Ok(url) => {
|
||||
tracing::info!(
|
||||
"Opened editor for repo {} at path: {}{}",
|
||||
repo_id,
|
||||
repo.path.to_string_lossy(),
|
||||
if url.is_some() { " (remote mode)" } else { "" }
|
||||
);
|
||||
|
||||
deployment
|
||||
.track_if_analytics_allowed(
|
||||
"repo_editor_opened",
|
||||
serde_json::json!({
|
||||
"repo_id": repo_id.to_string(),
|
||||
"editor_type": payload.as_ref().and_then(|req| req.editor_type.as_ref()),
|
||||
"remote_mode": url.is_some(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(ResponseJson(ApiResponse::success(OpenEditorResponse {
|
||||
url,
|
||||
})))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to open editor for repo {}: {:?}", repo_id, e);
|
||||
Err(ApiError::EditorOpen(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router() -> Router<DeploymentImpl> {
|
||||
Router::new()
|
||||
.route("/repos", post(register_repo))
|
||||
.route("/repos/init", post(init_repo))
|
||||
.route("/repos/batch", post(get_repos_batch))
|
||||
.route("/repos/{repo_id}/branches", get(get_repo_branches))
|
||||
.route("/repos/{repo_id}/open-editor", post(open_repo_in_editor))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod queue;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use axum::{
|
||||
Extension, Json, Router,
|
||||
extract::{Query, State},
|
||||
@@ -19,6 +21,7 @@ use executors::{
|
||||
actions::{
|
||||
ExecutorAction, ExecutorActionType, coding_agent_follow_up::CodingAgentFollowUpRequest,
|
||||
},
|
||||
executors::BaseCodingAgent,
|
||||
profile::ExecutorProfileId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
@@ -115,12 +118,29 @@ pub async fn follow_up(
|
||||
.ensure_container_exists(&workspace)
|
||||
.await?;
|
||||
|
||||
// Get executor profile data from the latest CodingAgent process in this session
|
||||
let initial_executor_profile_id =
|
||||
ExecutionProcess::latest_executor_profile_for_session(pool, session.id).await?;
|
||||
// Get executor from the latest CodingAgent process, or fall back to session's executor
|
||||
let base_executor =
|
||||
match ExecutionProcess::latest_executor_profile_for_session(pool, session.id).await? {
|
||||
Some(profile) => profile.executor,
|
||||
None => {
|
||||
// No prior execution - use session's executor field
|
||||
let executor_str = session.executor.as_ref().ok_or_else(|| {
|
||||
ApiError::Workspace(WorkspaceError::ValidationError(
|
||||
"No prior execution and no executor configured on session".to_string(),
|
||||
))
|
||||
})?;
|
||||
BaseCodingAgent::from_str(&executor_str.replace('-', "_").to_ascii_uppercase())
|
||||
.map_err(|_| {
|
||||
ApiError::Workspace(WorkspaceError::ValidationError(format!(
|
||||
"Invalid executor: {}",
|
||||
executor_str
|
||||
)))
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
let executor_profile_id = ExecutorProfileId {
|
||||
executor: initial_executor_profile_id.executor,
|
||||
executor: base_executor,
|
||||
variant: payload.variant,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod gh_cli_setup;
|
||||
pub mod images;
|
||||
pub mod pr;
|
||||
pub mod util;
|
||||
pub mod workspace_summary;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -19,9 +20,10 @@ use axum::{
|
||||
http::StatusCode,
|
||||
middleware::from_fn_with_state,
|
||||
response::{IntoResponse, Json as ResponseJson},
|
||||
routing::{get, post},
|
||||
routing::{get, post, put},
|
||||
};
|
||||
use db::models::{
|
||||
coding_agent_turn::CodingAgentTurn,
|
||||
execution_process::{ExecutionProcess, ExecutionProcessRunReason, ExecutionProcessStatus},
|
||||
merge::{Merge, MergeStatus, PrMerge, PullRequestInfo},
|
||||
project_repo::ProjectRepo,
|
||||
@@ -46,6 +48,7 @@ use services::services::{
|
||||
container::ContainerService,
|
||||
git::{ConflictOp, GitCliError, GitServiceError},
|
||||
github::GitHubService,
|
||||
workspace_manager::WorkspaceManager,
|
||||
};
|
||||
use sqlx::Error as SqlxError;
|
||||
use ts_rs::TS;
|
||||
@@ -88,6 +91,19 @@ pub struct DiffStreamQuery {
|
||||
pub stats_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WorkspaceStreamQuery {
|
||||
pub archived: Option<bool>,
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
pub struct UpdateWorkspace {
|
||||
pub archived: Option<bool>,
|
||||
pub pinned: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_task_attempts(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Query(query): Query<TaskAttemptQuery>,
|
||||
@@ -103,6 +119,26 @@ pub async fn get_task_attempt(
|
||||
Ok(ResponseJson(ApiResponse::success(workspace)))
|
||||
}
|
||||
|
||||
pub async fn update_workspace(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Json(request): Json<UpdateWorkspace>,
|
||||
) -> Result<ResponseJson<ApiResponse<Workspace>>, ApiError> {
|
||||
let pool = &deployment.db().pool;
|
||||
Workspace::update(
|
||||
pool,
|
||||
workspace.id,
|
||||
request.archived,
|
||||
request.pinned,
|
||||
request.name.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let updated = Workspace::find_by_id(pool, workspace.id)
|
||||
.await?
|
||||
.ok_or(WorkspaceError::TaskNotFound)?;
|
||||
Ok(ResponseJson(ApiResponse::success(updated)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
pub struct CreateTaskAttemptBody {
|
||||
pub task_id: Uuid,
|
||||
@@ -302,6 +338,61 @@ async fn handle_task_attempt_diff_ws(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stream_workspaces_ws(
|
||||
ws: WebSocketUpgrade,
|
||||
Query(query): Query<WorkspaceStreamQuery>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
if let Err(e) = handle_workspaces_ws(socket, deployment, query.archived, query.limit).await
|
||||
{
|
||||
tracing::warn!("workspaces WS closed: {}", e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_workspaces_ws(
|
||||
socket: WebSocket,
|
||||
deployment: DeploymentImpl,
|
||||
archived: Option<bool>,
|
||||
limit: Option<i64>,
|
||||
) -> anyhow::Result<()> {
|
||||
use futures_util::{SinkExt, StreamExt, TryStreamExt};
|
||||
|
||||
let mut stream = deployment
|
||||
.events()
|
||||
.stream_workspaces_raw(archived, limit)
|
||||
.await?
|
||||
.map_ok(|msg| msg.to_ws_message_unchecked());
|
||||
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
item = stream.next() => {
|
||||
match item {
|
||||
Some(Ok(msg)) => {
|
||||
if sender.send(msg).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
tracing::error!("stream error: {}", e);
|
||||
break;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
msg = receiver.next() => {
|
||||
if msg.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
pub struct MergeTaskAttemptRequest {
|
||||
pub repo_id: Uuid,
|
||||
@@ -370,6 +461,9 @@ pub async fn merge_task_attempt(
|
||||
)
|
||||
.await?;
|
||||
Task::update_status(pool, task.id, TaskStatus::Done).await?;
|
||||
if !workspace.pinned {
|
||||
Workspace::set_archived(pool, workspace.id, true).await?;
|
||||
}
|
||||
|
||||
// Stop any running dev servers for this workspace
|
||||
let dev_servers =
|
||||
@@ -1482,9 +1576,156 @@ pub async fn get_task_attempt_repos(
|
||||
Ok(ResponseJson(ApiResponse::success(repos)))
|
||||
}
|
||||
|
||||
pub async fn get_first_user_message(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
) -> Result<ResponseJson<ApiResponse<Option<String>>>, ApiError> {
|
||||
let pool = &deployment.db().pool;
|
||||
|
||||
let message = Workspace::get_first_user_message(pool, workspace.id).await?;
|
||||
|
||||
Ok(ResponseJson(ApiResponse::success(message)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type", rename_all = "snake_case")]
|
||||
pub enum DeleteWorkspaceError {
|
||||
HasRunningProcesses,
|
||||
}
|
||||
|
||||
pub async fn delete_workspace(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
) -> Result<
|
||||
(
|
||||
StatusCode,
|
||||
ResponseJson<ApiResponse<(), DeleteWorkspaceError>>,
|
||||
),
|
||||
ApiError,
|
||||
> {
|
||||
let pool = &deployment.db().pool;
|
||||
|
||||
// Check for running execution processes
|
||||
if ExecutionProcess::has_running_non_dev_server_processes_for_workspace(pool, workspace.id)
|
||||
.await?
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::CONFLICT,
|
||||
ResponseJson(ApiResponse::error_with_data(
|
||||
DeleteWorkspaceError::HasRunningProcesses,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
// Stop any running dev servers for this workspace
|
||||
let dev_servers =
|
||||
ExecutionProcess::find_running_dev_servers_by_workspace(pool, workspace.id).await?;
|
||||
|
||||
for dev_server in dev_servers {
|
||||
tracing::info!(
|
||||
"Stopping dev server {} before deleting workspace {}",
|
||||
dev_server.id,
|
||||
workspace.id
|
||||
);
|
||||
|
||||
if let Err(e) = deployment
|
||||
.container()
|
||||
.stop_execution(&dev_server, ExecutionProcessStatus::Killed)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to stop dev server {} for workspace {}: {}",
|
||||
dev_server.id,
|
||||
workspace.id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Gather data needed for background cleanup
|
||||
let workspace_dir = workspace.container_ref.clone().map(PathBuf::from);
|
||||
let repositories = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
|
||||
|
||||
// Nullify parent_workspace_id for any child tasks before deletion
|
||||
let children_affected = Task::nullify_children_by_workspace_id(pool, workspace.id).await?;
|
||||
if children_affected > 0 {
|
||||
tracing::info!(
|
||||
"Nullified {} child task references before deleting workspace {}",
|
||||
children_affected,
|
||||
workspace.id
|
||||
);
|
||||
}
|
||||
|
||||
// Delete workspace from database (FK CASCADE will handle sessions, execution_processes, etc.)
|
||||
let rows_affected = Workspace::delete(pool, workspace.id).await?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(ApiError::Database(SqlxError::RowNotFound));
|
||||
}
|
||||
|
||||
deployment
|
||||
.track_if_analytics_allowed(
|
||||
"workspace_deleted",
|
||||
serde_json::json!({
|
||||
"workspace_id": workspace.id.to_string(),
|
||||
"task_id": workspace.task_id.to_string(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Spawn background cleanup task for filesystem resources
|
||||
if let Some(workspace_dir) = workspace_dir {
|
||||
let workspace_id = workspace.id;
|
||||
tokio::spawn(async move {
|
||||
tracing::info!(
|
||||
"Starting background cleanup for workspace {} at {}",
|
||||
workspace_id,
|
||||
workspace_dir.display()
|
||||
);
|
||||
|
||||
if let Err(e) = WorkspaceManager::cleanup_workspace(&workspace_dir, &repositories).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Background workspace cleanup failed for {} at {}: {}",
|
||||
workspace_id,
|
||||
workspace_dir.display(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Background cleanup completed for workspace {}",
|
||||
workspace_id
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return 202 Accepted to indicate deletion was scheduled
|
||||
Ok((StatusCode::ACCEPTED, ResponseJson(ApiResponse::success(()))))
|
||||
}
|
||||
|
||||
/// Mark all coding agent turns for a workspace as seen
|
||||
#[axum::debug_handler]
|
||||
pub async fn mark_seen(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
) -> Result<ResponseJson<ApiResponse<()>>, ApiError> {
|
||||
let pool = &deployment.db().pool;
|
||||
|
||||
CodingAgentTurn::mark_seen_by_workspace_id(pool, workspace.id).await?;
|
||||
|
||||
Ok(ResponseJson(ApiResponse::success(())))
|
||||
}
|
||||
|
||||
pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
let task_attempt_id_router = Router::new()
|
||||
.route("/", get(get_task_attempt))
|
||||
.route(
|
||||
"/",
|
||||
get(get_task_attempt)
|
||||
.put(update_workspace)
|
||||
.delete(delete_workspace),
|
||||
)
|
||||
.route("/run-agent-setup", post(run_agent_setup))
|
||||
.route("/gh-cli-setup", post(gh_cli_setup_handler))
|
||||
.route("/start-dev-server", post(start_dev_server))
|
||||
@@ -1506,6 +1747,8 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
.route("/change-target-branch", post(change_target_branch))
|
||||
.route("/rename-branch", post(rename_branch))
|
||||
.route("/repos", get(get_task_attempt_repos))
|
||||
.route("/first-message", get(get_first_user_message))
|
||||
.route("/mark-seen", put(mark_seen))
|
||||
.layer(from_fn_with_state(
|
||||
deployment.clone(),
|
||||
load_workspace_middleware,
|
||||
@@ -1513,6 +1756,8 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
|
||||
let task_attempts_router = Router::new()
|
||||
.route("/", get(get_task_attempts).post(create_task_attempt))
|
||||
.route("/stream/ws", get(stream_workspaces_ws))
|
||||
.route("/summary", post(workspace_summary::get_workspace_summaries))
|
||||
.nest("/{id}", task_attempt_id_router)
|
||||
.nest("/{id}/images", images::router(deployment));
|
||||
|
||||
|
||||
@@ -135,9 +135,16 @@ async fn trigger_pr_description_follow_up(
|
||||
};
|
||||
|
||||
// Get executor profile from the latest coding agent process in this session
|
||||
let executor_profile_id =
|
||||
let Some(executor_profile_id) =
|
||||
ExecutionProcess::latest_executor_profile_for_session(&deployment.db().pool, session.id)
|
||||
.await?;
|
||||
.await?
|
||||
else {
|
||||
tracing::warn!(
|
||||
"No executor profile found for session {}, skipping PR description follow-up",
|
||||
session.id
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Get latest agent session ID if one exists (for coding agent continuity)
|
||||
let latest_agent_session_id = ExecutionProcess::find_latest_coding_agent_turn_session_id(
|
||||
@@ -419,9 +426,12 @@ pub async fn attach_existing_pr(
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If PR is merged, mark task as done
|
||||
// If PR is merged, mark task as done and archive workspace
|
||||
if matches!(pr_info.status, MergeStatus::Merged) {
|
||||
Task::update_status(pool, task.id, TaskStatus::Done).await?;
|
||||
if !workspace.pinned {
|
||||
Workspace::set_archived(pool, workspace.id, true).await?;
|
||||
}
|
||||
|
||||
// Try broadcast update to other users in organization
|
||||
if let Ok(publisher) = deployment.share_publisher() {
|
||||
|
||||
222
crates/server/src/routes/task_attempts/workspace_summary.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use axum::{Json, extract::State, response::Json as ResponseJson};
|
||||
use db::models::{
|
||||
coding_agent_turn::CodingAgentTurn,
|
||||
execution_process::{ExecutionProcess, ExecutionProcessStatus},
|
||||
workspace::Workspace,
|
||||
workspace_repo::WorkspaceRepo,
|
||||
};
|
||||
use deployment::Deployment;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use services::services::git::DiffTarget;
|
||||
use ts_rs::TS;
|
||||
use utils::response::ApiResponse;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{DeploymentImpl, error::ApiError};
|
||||
|
||||
/// Request for fetching workspace summaries
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
pub struct WorkspaceSummaryRequest {
|
||||
pub archived: bool,
|
||||
}
|
||||
|
||||
/// Summary info for a single workspace
|
||||
#[derive(Debug, Serialize, TS)]
|
||||
pub struct WorkspaceSummary {
|
||||
pub workspace_id: Uuid,
|
||||
/// Session ID of the latest execution process
|
||||
pub latest_session_id: Option<Uuid>,
|
||||
/// Is a tool approval currently pending?
|
||||
pub has_pending_approval: bool,
|
||||
/// Number of files with changes
|
||||
pub files_changed: Option<usize>,
|
||||
/// Total lines added across all files
|
||||
pub lines_added: Option<usize>,
|
||||
/// Total lines removed across all files
|
||||
pub lines_removed: Option<usize>,
|
||||
/// When the latest execution process completed
|
||||
#[ts(optional)]
|
||||
pub latest_process_completed_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
/// Status of the latest execution process
|
||||
pub latest_process_status: Option<ExecutionProcessStatus>,
|
||||
/// Is a dev server currently running?
|
||||
pub has_running_dev_server: bool,
|
||||
/// Does this workspace have unseen coding agent turns?
|
||||
pub has_unseen_turns: bool,
|
||||
}
|
||||
|
||||
/// Response containing summaries for requested workspaces
|
||||
#[derive(Debug, Serialize, TS)]
|
||||
pub struct WorkspaceSummaryResponse {
|
||||
pub summaries: Vec<WorkspaceSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct DiffStats {
|
||||
files_changed: usize,
|
||||
lines_added: usize,
|
||||
lines_removed: usize,
|
||||
}
|
||||
|
||||
/// Fetch summary information for workspaces filtered by archived status.
|
||||
/// This endpoint returns data that cannot be efficiently included in the streaming endpoint.
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_workspace_summaries(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Json(request): Json<WorkspaceSummaryRequest>,
|
||||
) -> Result<ResponseJson<ApiResponse<WorkspaceSummaryResponse>>, ApiError> {
|
||||
let pool = &deployment.db().pool;
|
||||
let archived = request.archived;
|
||||
|
||||
// 1. Fetch all workspaces with the given archived status
|
||||
let workspaces: Vec<Workspace> = Workspace::find_all_with_status(pool, Some(archived), None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|ws| ws.workspace)
|
||||
.collect();
|
||||
|
||||
if workspaces.is_empty() {
|
||||
return Ok(ResponseJson(ApiResponse::success(
|
||||
WorkspaceSummaryResponse { summaries: vec![] },
|
||||
)));
|
||||
}
|
||||
|
||||
// 2. Fetch latest process info for workspaces with this archived status
|
||||
let latest_processes = ExecutionProcess::find_latest_for_workspaces(pool, archived).await?;
|
||||
|
||||
// 3. Check which workspaces have running dev servers
|
||||
let dev_server_workspaces =
|
||||
ExecutionProcess::find_workspaces_with_running_dev_servers(pool, archived).await?;
|
||||
|
||||
// 4. Check pending approvals for running processes
|
||||
let running_ep_ids: Vec<_> = latest_processes
|
||||
.values()
|
||||
.filter(|info| info.status == ExecutionProcessStatus::Running)
|
||||
.map(|info| info.execution_process_id)
|
||||
.collect();
|
||||
let pending_approval_eps = deployment
|
||||
.approvals()
|
||||
.get_pending_execution_process_ids(&running_ep_ids);
|
||||
|
||||
// 5. Check which workspaces have unseen coding agent turns
|
||||
let unseen_workspaces = CodingAgentTurn::find_workspaces_with_unseen(pool, archived).await?;
|
||||
|
||||
// 6. Compute diff stats for each workspace (in parallel)
|
||||
let diff_futures: Vec<_> = workspaces
|
||||
.iter()
|
||||
.map(|ws| {
|
||||
let workspace = ws.clone();
|
||||
let deployment = deployment.clone();
|
||||
async move {
|
||||
if workspace.container_ref.is_some() {
|
||||
compute_workspace_diff_stats(&deployment, &workspace)
|
||||
.await
|
||||
.ok()
|
||||
.map(|stats| (workspace.id, stats))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let diff_results: Vec<Option<(Uuid, DiffStats)>> =
|
||||
futures_util::future::join_all(diff_futures).await;
|
||||
let diff_stats: HashMap<Uuid, DiffStats> = diff_results.into_iter().flatten().collect();
|
||||
|
||||
// 7. Assemble response
|
||||
let summaries: Vec<WorkspaceSummary> = workspaces
|
||||
.iter()
|
||||
.map(|ws| {
|
||||
let id = ws.id;
|
||||
let latest = latest_processes.get(&id);
|
||||
let has_pending = latest
|
||||
.map(|p| pending_approval_eps.contains(&p.execution_process_id))
|
||||
.unwrap_or(false);
|
||||
let stats = diff_stats.get(&id);
|
||||
|
||||
WorkspaceSummary {
|
||||
workspace_id: id,
|
||||
latest_session_id: latest.map(|p| p.session_id),
|
||||
has_pending_approval: has_pending,
|
||||
files_changed: stats.map(|s| s.files_changed),
|
||||
lines_added: stats.map(|s| s.lines_added),
|
||||
lines_removed: stats.map(|s| s.lines_removed),
|
||||
latest_process_completed_at: latest.and_then(|p| p.completed_at),
|
||||
latest_process_status: latest.map(|p| p.status.clone()),
|
||||
has_running_dev_server: dev_server_workspaces.contains(&id),
|
||||
has_unseen_turns: unseen_workspaces.contains(&id),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ResponseJson(ApiResponse::success(
|
||||
WorkspaceSummaryResponse { summaries },
|
||||
)))
|
||||
}
|
||||
|
||||
/// Compute diff stats for a workspace.
|
||||
async fn compute_workspace_diff_stats(
|
||||
deployment: &DeploymentImpl,
|
||||
workspace: &Workspace,
|
||||
) -> Result<DiffStats, ApiError> {
|
||||
let pool = &deployment.db().pool;
|
||||
|
||||
let container_ref = workspace
|
||||
.container_ref
|
||||
.as_ref()
|
||||
.ok_or_else(|| ApiError::BadRequest("No container ref".to_string()))?;
|
||||
|
||||
let workspace_repos =
|
||||
WorkspaceRepo::find_repos_with_target_branch_for_workspace(pool, workspace.id).await?;
|
||||
|
||||
let mut stats = DiffStats::default();
|
||||
|
||||
for repo_with_branch in workspace_repos {
|
||||
let worktree_path = PathBuf::from(container_ref).join(&repo_with_branch.repo.name);
|
||||
let repo_path = repo_with_branch.repo.path.clone();
|
||||
|
||||
// Get base commit (merge base) between workspace branch and target branch
|
||||
let base_commit_result = tokio::task::spawn_blocking({
|
||||
let git = deployment.git().clone();
|
||||
let repo_path = repo_path.clone();
|
||||
let workspace_branch = workspace.branch.clone();
|
||||
let target_branch = repo_with_branch.target_branch.clone();
|
||||
move || git.get_base_commit(&repo_path, &workspace_branch, &target_branch)
|
||||
})
|
||||
.await;
|
||||
|
||||
let base_commit = match base_commit_result {
|
||||
Ok(Ok(commit)) => commit,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Get diffs
|
||||
let diffs_result = tokio::task::spawn_blocking({
|
||||
let git = deployment.git().clone();
|
||||
let worktree = worktree_path.clone();
|
||||
move || {
|
||||
git.get_diffs(
|
||||
DiffTarget::Worktree {
|
||||
worktree_path: &worktree,
|
||||
base_commit: &base_commit,
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Ok(Ok(diffs)) = diffs_result {
|
||||
for diff in diffs {
|
||||
stats.files_changed += 1;
|
||||
stats.lines_added += diff.additions.unwrap_or(0);
|
||||
stats.lines_removed += diff.deletions.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
@@ -314,15 +314,6 @@ pub async fn delete_task(
|
||||
) -> Result<(StatusCode, ResponseJson<ApiResponse<()>>), ApiError> {
|
||||
ensure_shared_task_auth(&task, &deployment).await?;
|
||||
|
||||
// Validate no running execution processes
|
||||
if deployment
|
||||
.container()
|
||||
.has_running_processes(task.id)
|
||||
.await?
|
||||
{
|
||||
return Err(ApiError::Conflict("Task has running execution processes. Please wait for them to complete or stop them first.".to_string()));
|
||||
}
|
||||
|
||||
let pool = &deployment.db().pool;
|
||||
|
||||
// Gather task attempts data needed for background cleanup
|
||||
@@ -333,6 +324,11 @@ pub async fn delete_task(
|
||||
ApiError::Workspace(e)
|
||||
})?;
|
||||
|
||||
// Stop any running execution processes before deletion
|
||||
for workspace in &attempts {
|
||||
deployment.container().try_stop(workspace, true).await;
|
||||
}
|
||||
|
||||
let repositories = WorkspaceRepo::find_unique_repos_for_task(pool, task.id).await?;
|
||||
|
||||
// Collect workspace directories that need cleanup
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
pub mod executor_approvals;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration as StdDuration};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration as StdDuration,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use db::models::{
|
||||
@@ -256,6 +260,26 @@ impl Approvals {
|
||||
let map = self.msg_stores.read().await;
|
||||
map.get(execution_process_id).cloned()
|
||||
}
|
||||
|
||||
/// Check which execution processes have pending approvals.
|
||||
/// Returns a set of execution_process_ids that have at least one pending approval.
|
||||
pub fn get_pending_execution_process_ids(
|
||||
&self,
|
||||
execution_process_ids: &[Uuid],
|
||||
) -> HashSet<Uuid> {
|
||||
let id_set: HashSet<_> = execution_process_ids.iter().collect();
|
||||
self.pending
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let ep_id = entry.value().execution_process_id;
|
||||
if id_set.contains(&ep_id) {
|
||||
Some(ep_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn ensure_task_in_review(pool: &SqlitePool, execution_process_id: Uuid) {
|
||||
|
||||
@@ -873,7 +873,7 @@ pub trait ContainerService {
|
||||
LogMsg::Finished => {
|
||||
break;
|
||||
}
|
||||
LogMsg::JsonPatch(_) => continue,
|
||||
LogMsg::JsonPatch(_) | LogMsg::Ready => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1050,6 +1050,8 @@ pub trait ContainerService {
|
||||
)
|
||||
.await?;
|
||||
|
||||
Workspace::set_archived(&self.db().pool, workspace.id, false).await?;
|
||||
|
||||
if let Some(prompt) = match executor_action.typ() {
|
||||
ExecutorActionType::CodingAgentInitialRequest(coding_agent_request) => {
|
||||
Some(coding_agent_request.prompt.clone())
|
||||
|
||||
@@ -155,6 +155,9 @@ impl DiffStreamManager {
|
||||
async fn run(&mut self) -> Result<(), DiffStreamError> {
|
||||
self.reset_stream().await?;
|
||||
|
||||
// Send Ready message to indicate initial data has been sent
|
||||
let _ready_error = self.tx.send(Ok(LogMsg::Ready)).await;
|
||||
|
||||
let (fs_debouncer, mut fs_rx, canonical_worktree) =
|
||||
filesystem_watcher::async_watcher(self.args.worktree_path.clone())
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
|
||||
@@ -77,6 +77,21 @@ impl EventService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn push_workspace_update_for_session(
|
||||
pool: &SqlitePool,
|
||||
msg_store: Arc<MsgStore>,
|
||||
session_id: Uuid,
|
||||
) -> Result<(), SqlxError> {
|
||||
use db::models::session::Session;
|
||||
if let Some(session) = Session::find_by_id(pool, session_id).await?
|
||||
&& let Some(workspace_with_status) =
|
||||
Workspace::find_by_id_with_status(pool, session.workspace_id).await?
|
||||
{
|
||||
msg_store.push_patch(workspace_patch::replace(&workspace_with_status));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates the hook function that should be used with DBService::new_with_after_connect
|
||||
pub fn create_hook(
|
||||
msg_store: Arc<MsgStore>,
|
||||
@@ -317,7 +332,21 @@ impl EventService {
|
||||
return;
|
||||
}
|
||||
RecordTypes::Workspace(workspace) => {
|
||||
// Workspaces should update the parent task with fresh data
|
||||
// Emit workspace patch with status
|
||||
if let Ok(Some(workspace_with_status)) =
|
||||
Workspace::find_by_id_with_status(&db.pool, workspace.id)
|
||||
.await
|
||||
{
|
||||
let patch = match hook.operation {
|
||||
SqliteOperation::Insert => {
|
||||
workspace_patch::add(&workspace_with_status)
|
||||
}
|
||||
_ => workspace_patch::replace(&workspace_with_status),
|
||||
};
|
||||
msg_store_for_hook.push_patch(patch);
|
||||
}
|
||||
|
||||
// Also update parent task
|
||||
if let Ok(Some(task)) =
|
||||
Task::find_by_id(&db.pool, workspace.task_id).await
|
||||
&& let Ok(task_list) =
|
||||
@@ -331,14 +360,14 @@ impl EventService {
|
||||
{
|
||||
let patch = task_patch::replace(&task_with_status);
|
||||
msg_store_for_hook.push_patch(patch);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
RecordTypes::DeletedWorkspace {
|
||||
task_id: Some(task_id),
|
||||
..
|
||||
} => {
|
||||
// Workspace deletion should update the parent task with fresh data
|
||||
// Update parent task
|
||||
if let Ok(Some(task)) =
|
||||
Task::find_by_id(&db.pool, *task_id).await
|
||||
&& let Ok(task_list) =
|
||||
@@ -352,8 +381,8 @@ impl EventService {
|
||||
{
|
||||
let patch = task_patch::replace(&task_with_status);
|
||||
msg_store_for_hook.push_patch(patch);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
RecordTypes::ExecutionProcess(process) => {
|
||||
let patch = match hook.operation {
|
||||
@@ -380,6 +409,19 @@ impl EventService {
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = EventService::push_workspace_update_for_session(
|
||||
&db.pool,
|
||||
msg_store_for_hook.clone(),
|
||||
process.session_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to push workspace update after execution process change: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
RecordTypes::DeletedExecutionProcess {
|
||||
@@ -390,8 +432,8 @@ impl EventService {
|
||||
let patch = execution_process_patch::remove(*process_id);
|
||||
msg_store_for_hook.push_patch(patch);
|
||||
|
||||
if let Some(session_id) = session_id
|
||||
&& let Err(err) =
|
||||
if let Some(session_id) = session_id {
|
||||
if let Err(err) =
|
||||
EventService::push_task_update_for_session(
|
||||
&db.pool,
|
||||
msg_store_for_hook.clone(),
|
||||
@@ -405,6 +447,21 @@ impl EventService {
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
EventService::push_workspace_update_for_session(
|
||||
&db.pool,
|
||||
msg_store_for_hook.clone(),
|
||||
*session_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to push workspace update after execution process removal: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use db::models::{
|
||||
execution_process::ExecutionProcess, project::Project, scratch::Scratch,
|
||||
task::TaskWithAttemptStatus, workspace::Workspace,
|
||||
task::TaskWithAttemptStatus, workspace::WorkspaceWithStatus,
|
||||
};
|
||||
use json_patch::{AddOperation, Patch, PatchOperation, RemoveOperation, ReplaceOperation};
|
||||
use uuid::Uuid;
|
||||
@@ -143,8 +143,7 @@ pub mod workspace_patch {
|
||||
)
|
||||
}
|
||||
|
||||
/// Create patch for adding a new workspace
|
||||
pub fn add(workspace: &Workspace) -> Patch {
|
||||
pub fn add(workspace: &WorkspaceWithStatus) -> Patch {
|
||||
Patch(vec![PatchOperation::Add(AddOperation {
|
||||
path: workspace_path(workspace.id)
|
||||
.try_into()
|
||||
@@ -154,8 +153,7 @@ pub mod workspace_patch {
|
||||
})])
|
||||
}
|
||||
|
||||
/// Create patch for updating an existing workspace
|
||||
pub fn replace(workspace: &Workspace) -> Patch {
|
||||
pub fn replace(workspace: &WorkspaceWithStatus) -> Patch {
|
||||
Patch(vec![PatchOperation::Replace(ReplaceOperation {
|
||||
path: workspace_path(workspace.id)
|
||||
.try_into()
|
||||
@@ -165,7 +163,6 @@ pub mod workspace_patch {
|
||||
})])
|
||||
}
|
||||
|
||||
/// Create patch for removing a workspace
|
||||
pub fn remove(workspace_id: Uuid) -> Patch {
|
||||
Patch(vec![PatchOperation::Remove(RemoveOperation {
|
||||
path: workspace_path(workspace_id)
|
||||
|
||||
@@ -2,8 +2,8 @@ use db::models::{
|
||||
execution_process::ExecutionProcess,
|
||||
project::Project,
|
||||
scratch::Scratch,
|
||||
session::Session,
|
||||
task::{Task, TaskWithAttemptStatus},
|
||||
workspace::Workspace,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use serde_json::json;
|
||||
@@ -140,8 +140,8 @@ impl EventService {
|
||||
}
|
||||
});
|
||||
|
||||
// Start with initial snapshot, then live updates
|
||||
let initial_stream = futures::stream::once(async move { Ok(initial_msg) });
|
||||
// Start with initial snapshot, Ready signal, then live updates
|
||||
let initial_stream = futures::stream::iter(vec![Ok(initial_msg), Ok(LogMsg::Ready)]);
|
||||
let combined_stream = initial_stream.chain(filtered_stream).boxed();
|
||||
|
||||
Ok(combined_stream)
|
||||
@@ -219,35 +219,24 @@ impl EventService {
|
||||
}
|
||||
});
|
||||
|
||||
// Start with initial snapshot, then live updates
|
||||
let initial_stream = futures::stream::once(async move { Ok(initial_msg) });
|
||||
// Start with initial snapshot, Ready signal, then live updates
|
||||
let initial_stream = futures::stream::iter(vec![Ok(initial_msg), Ok(LogMsg::Ready)]);
|
||||
let combined_stream = initial_stream.chain(filtered_stream).boxed();
|
||||
|
||||
Ok(combined_stream)
|
||||
}
|
||||
|
||||
/// Stream execution processes for a specific workspace with initial snapshot (raw LogMsg format for WebSocket)
|
||||
pub async fn stream_execution_processes_for_workspace_raw(
|
||||
/// Stream execution processes for a specific session with initial snapshot (raw LogMsg format for WebSocket)
|
||||
pub async fn stream_execution_processes_for_session_raw(
|
||||
&self,
|
||||
workspace_id: Uuid,
|
||||
session_id: Uuid,
|
||||
show_soft_deleted: bool,
|
||||
) -> Result<futures::stream::BoxStream<'static, Result<LogMsg, std::io::Error>>, EventError>
|
||||
{
|
||||
// Get all sessions for this workspace
|
||||
let sessions = Session::find_by_workspace_id(&self.db.pool, workspace_id).await?;
|
||||
|
||||
// Collect all execution processes across all sessions
|
||||
let mut all_processes = Vec::new();
|
||||
for session in &sessions {
|
||||
let processes =
|
||||
ExecutionProcess::find_by_session_id(&self.db.pool, session.id, show_soft_deleted)
|
||||
.await?;
|
||||
all_processes.extend(processes);
|
||||
}
|
||||
let processes = all_processes;
|
||||
|
||||
// Collect session IDs for filtering
|
||||
let session_ids: Vec<Uuid> = sessions.iter().map(|s| s.id).collect();
|
||||
// Get execution processes for this session
|
||||
let processes =
|
||||
ExecutionProcess::find_by_session_id(&self.db.pool, session_id, show_soft_deleted)
|
||||
.await?;
|
||||
|
||||
// Convert processes array to object keyed by process ID
|
||||
let processes_map: serde_json::Map<String, serde_json::Value> = processes
|
||||
@@ -270,11 +259,10 @@ impl EventService {
|
||||
// Get filtered event stream
|
||||
let filtered_stream =
|
||||
BroadcastStream::new(self.msg_store.get_receiver()).filter_map(move |msg_result| {
|
||||
let session_ids = session_ids.clone();
|
||||
async move {
|
||||
match msg_result {
|
||||
Ok(LogMsg::JsonPatch(patch)) => {
|
||||
// Filter events based on session_id (must belong to one of the workspace's sessions)
|
||||
// Filter events based on session_id
|
||||
if let Some(patch_op) = patch.0.first() {
|
||||
// Check if this is a modern execution process patch
|
||||
if patch_op.path().starts_with("/execution_processes/") {
|
||||
@@ -285,7 +273,7 @@ impl EventService {
|
||||
serde_json::from_value::<ExecutionProcess>(
|
||||
op.value.clone(),
|
||||
)
|
||||
&& session_ids.contains(&process.session_id)
|
||||
&& process.session_id == session_id
|
||||
{
|
||||
if !show_soft_deleted && process.dropped {
|
||||
let remove_patch =
|
||||
@@ -303,7 +291,7 @@ impl EventService {
|
||||
serde_json::from_value::<ExecutionProcess>(
|
||||
op.value.clone(),
|
||||
)
|
||||
&& session_ids.contains(&process.session_id)
|
||||
&& process.session_id == session_id
|
||||
{
|
||||
if !show_soft_deleted && process.dropped {
|
||||
let remove_patch =
|
||||
@@ -330,7 +318,7 @@ impl EventService {
|
||||
{
|
||||
match &event_patch.value.record {
|
||||
RecordTypes::ExecutionProcess(process) => {
|
||||
if session_ids.contains(&process.session_id) {
|
||||
if process.session_id == session_id {
|
||||
if !show_soft_deleted && process.dropped {
|
||||
let remove_patch =
|
||||
execution_process_patch::remove(process.id);
|
||||
@@ -345,7 +333,7 @@ impl EventService {
|
||||
session_id: Some(deleted_session_id),
|
||||
..
|
||||
} => {
|
||||
if session_ids.contains(deleted_session_id) {
|
||||
if *deleted_session_id == session_id {
|
||||
return Some(Ok(LogMsg::JsonPatch(patch)));
|
||||
}
|
||||
}
|
||||
@@ -361,8 +349,8 @@ impl EventService {
|
||||
}
|
||||
});
|
||||
|
||||
// Start with initial snapshot, then live updates
|
||||
let initial_stream = futures::stream::once(async move { Ok(initial_msg) });
|
||||
// Start with initial snapshot, Ready signal, then live updates
|
||||
let initial_stream = futures::stream::iter(vec![Ok(initial_msg), Ok(LogMsg::Ready)]);
|
||||
let combined_stream = initial_stream.chain(filtered_stream).boxed();
|
||||
|
||||
Ok(combined_stream)
|
||||
@@ -441,8 +429,97 @@ impl EventService {
|
||||
}
|
||||
});
|
||||
|
||||
let initial_stream = futures::stream::once(async move { Ok(initial_msg) });
|
||||
let initial_stream = futures::stream::iter(vec![Ok(initial_msg), Ok(LogMsg::Ready)]);
|
||||
let combined_stream = initial_stream.chain(filtered_stream).boxed();
|
||||
Ok(combined_stream)
|
||||
}
|
||||
|
||||
pub async fn stream_workspaces_raw(
|
||||
&self,
|
||||
archived: Option<bool>,
|
||||
limit: Option<i64>,
|
||||
) -> Result<futures::stream::BoxStream<'static, Result<LogMsg, std::io::Error>>, EventError>
|
||||
{
|
||||
let workspaces = Workspace::find_all_with_status(&self.db.pool, archived, limit).await?;
|
||||
let workspaces_map: serde_json::Map<String, serde_json::Value> = workspaces
|
||||
.into_iter()
|
||||
.map(|ws| (ws.id.to_string(), serde_json::to_value(ws).unwrap()))
|
||||
.collect();
|
||||
|
||||
let initial_patch = json!([{
|
||||
"op": "replace",
|
||||
"path": "/workspaces",
|
||||
"value": workspaces_map
|
||||
}]);
|
||||
let initial_msg = LogMsg::JsonPatch(serde_json::from_value(initial_patch).unwrap());
|
||||
|
||||
let filtered_stream = BroadcastStream::new(self.msg_store.get_receiver()).filter_map(
|
||||
move |msg_result| async move {
|
||||
match msg_result {
|
||||
Ok(LogMsg::JsonPatch(patch)) => {
|
||||
if let Some(op) = patch.0.first()
|
||||
&& op.path().starts_with("/workspaces")
|
||||
{
|
||||
// If archived filter is set, handle state transitions
|
||||
if let Some(archived_filter) = archived {
|
||||
// Extract workspace data from Add/Replace operations
|
||||
let value = match op {
|
||||
json_patch::PatchOperation::Add(a) => Some(&a.value),
|
||||
json_patch::PatchOperation::Replace(r) => Some(&r.value),
|
||||
json_patch::PatchOperation::Remove(_) => {
|
||||
// Allow remove operations through - client will handle
|
||||
return Some(Ok(LogMsg::JsonPatch(patch)));
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(v) = value
|
||||
&& let Some(ws_archived) =
|
||||
v.get("archived").and_then(|a| a.as_bool())
|
||||
{
|
||||
if ws_archived == archived_filter {
|
||||
// Workspace matches this filter
|
||||
// Convert Replace to Add since workspace may be new to this filtered stream
|
||||
if let json_patch::PatchOperation::Replace(r) = op {
|
||||
let add_patch = json_patch::Patch(vec![
|
||||
json_patch::PatchOperation::Add(
|
||||
json_patch::AddOperation {
|
||||
path: r.path.clone(),
|
||||
value: r.value.clone(),
|
||||
},
|
||||
),
|
||||
]);
|
||||
return Some(Ok(LogMsg::JsonPatch(add_patch)));
|
||||
}
|
||||
return Some(Ok(LogMsg::JsonPatch(patch)));
|
||||
} else {
|
||||
// Workspace no longer matches this filter - send remove
|
||||
let remove_patch = json_patch::Patch(vec![
|
||||
json_patch::PatchOperation::Remove(
|
||||
json_patch::RemoveOperation {
|
||||
path: op
|
||||
.path()
|
||||
.to_string()
|
||||
.try_into()
|
||||
.expect("Workspace path should be valid"),
|
||||
},
|
||||
),
|
||||
]);
|
||||
return Some(Ok(LogMsg::JsonPatch(remove_patch)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(Ok(LogMsg::JsonPatch(patch)));
|
||||
}
|
||||
None
|
||||
}
|
||||
Ok(other) => Some(Ok(other)),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let initial_stream = futures::stream::iter(vec![Ok(initial_msg), Ok(LogMsg::Ready)]);
|
||||
Ok(initial_stream.chain(filtered_stream).boxed())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ impl FileRanker {
|
||||
}
|
||||
|
||||
/// Calculate relevance score for a search result
|
||||
fn calculate_score(&self, result: &SearchResult, stats: &FileStats) -> i64 {
|
||||
pub fn calculate_score(&self, result: &SearchResult, stats: &FileStats) -> i64 {
|
||||
let base_score = match result.match_type {
|
||||
SearchMatchType::FileName => BASE_MATCH_SCORE_FILENAME,
|
||||
SearchMatchType::DirectoryName => BASE_MATCH_SCORE_DIRNAME,
|
||||
|
||||
@@ -258,6 +258,7 @@ impl FileSearchCache {
|
||||
path: indexed_file.path.clone(),
|
||||
is_file: indexed_file.is_file,
|
||||
match_type: indexed_file.match_type.clone(),
|
||||
score: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -265,6 +266,11 @@ impl FileSearchCache {
|
||||
// Apply git history-based ranking
|
||||
self.file_ranker.rerank(&mut results, &cached.stats);
|
||||
|
||||
// Populate scores for sorted results
|
||||
for result in &mut results {
|
||||
result.score = self.file_ranker.calculate_score(result, &cached.stats);
|
||||
}
|
||||
|
||||
// Limit to top 10 results
|
||||
results.truncate(10);
|
||||
results
|
||||
|
||||
@@ -180,6 +180,7 @@ impl FilesystemService {
|
||||
None => return Ok(vec![]),
|
||||
};
|
||||
let skip_dirs = Self::get_directories_to_skip();
|
||||
let vibe_kanban_temp_dir = utils::path::get_vibe_kanban_temp_dir();
|
||||
let mut walker_builder = WalkBuilder::new(base_dir);
|
||||
walker_builder
|
||||
.follow_links(false)
|
||||
@@ -200,6 +201,14 @@ impl FilesystemService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip vibe-kanban temp directory and all subdirectories
|
||||
// Normalize to handle macOS /private/var vs /var aliasing
|
||||
if utils::path::normalize_macos_private_alias(path)
|
||||
.starts_with(&vibe_kanban_temp_dir)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip common non-git folders
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str())
|
||||
&& skip_dirs.contains(name)
|
||||
|
||||
@@ -330,6 +330,7 @@ impl ProjectService {
|
||||
path: format!("{}/{}", repo_name, r.path),
|
||||
is_file: r.is_file,
|
||||
match_type: r.match_type.clone(),
|
||||
score: r.score,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -342,7 +343,7 @@ impl ProjectService {
|
||||
};
|
||||
priority(&a.match_type)
|
||||
.cmp(&priority(&b.match_type))
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
.then_with(|| b.score.cmp(&a.score)) // Higher scores first
|
||||
});
|
||||
|
||||
all_results.truncate(10);
|
||||
@@ -438,6 +439,7 @@ impl ProjectService {
|
||||
path: relative_path.to_string_lossy().to_string(),
|
||||
is_file: path.is_file(),
|
||||
match_type: SearchMatchType::FileName,
|
||||
score: 0,
|
||||
});
|
||||
} else if relative_path_str.contains(&query_lower) {
|
||||
let match_type = if path
|
||||
@@ -456,6 +458,7 @@ impl ProjectService {
|
||||
path: relative_path.to_string_lossy().to_string(),
|
||||
is_file: path.is_file(),
|
||||
match_type,
|
||||
score: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -465,6 +468,10 @@ impl ProjectService {
|
||||
match file_ranker.get_stats(repo_path).await {
|
||||
Ok(stats) => {
|
||||
file_ranker.rerank(&mut results, &stats);
|
||||
// Populate scores for sorted results
|
||||
for result in &mut results {
|
||||
result.score = file_ranker.calculate_score(result, &stats);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Fallback to basic priority sorting
|
||||
|
||||
@@ -6,6 +6,7 @@ pub const EV_STDOUT: &str = "stdout";
|
||||
pub const EV_STDERR: &str = "stderr";
|
||||
pub const EV_JSON_PATCH: &str = "json_patch";
|
||||
pub const EV_SESSION_ID: &str = "session_id";
|
||||
pub const EV_READY: &str = "ready";
|
||||
pub const EV_FINISHED: &str = "finished";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -14,6 +15,7 @@ pub enum LogMsg {
|
||||
Stderr(String),
|
||||
JsonPatch(Patch),
|
||||
SessionId(String),
|
||||
Ready,
|
||||
Finished,
|
||||
}
|
||||
|
||||
@@ -24,6 +26,7 @@ impl LogMsg {
|
||||
LogMsg::Stderr(_) => EV_STDERR,
|
||||
LogMsg::JsonPatch(_) => EV_JSON_PATCH,
|
||||
LogMsg::SessionId(_) => EV_SESSION_ID,
|
||||
LogMsg::Ready => EV_READY,
|
||||
LogMsg::Finished => EV_FINISHED,
|
||||
}
|
||||
}
|
||||
@@ -37,6 +40,7 @@ impl LogMsg {
|
||||
Event::default().event(EV_JSON_PATCH).data(data)
|
||||
}
|
||||
LogMsg::SessionId(s) => Event::default().event(EV_SESSION_ID).data(s.clone()),
|
||||
LogMsg::Ready => Event::default().event(EV_READY).data(""),
|
||||
LogMsg::Finished => Event::default().event(EV_FINISHED).data(""),
|
||||
}
|
||||
}
|
||||
@@ -52,8 +56,9 @@ impl LogMsg {
|
||||
/// This method mirrors the behavior of the original logmsg_to_ws function
|
||||
/// but with better error handling than unwrap().
|
||||
pub fn to_ws_message_unchecked(&self) -> Message {
|
||||
// Finished becomes JSON {finished: true}
|
||||
// Finished and Ready use special JSON formats for frontend compatibility
|
||||
let json = match self {
|
||||
LogMsg::Ready => r#"{"Ready":true}"#.to_string(),
|
||||
LogMsg::Finished => r#"{"finished":true}"#.to_string(),
|
||||
_ => serde_json::to_string(self)
|
||||
.unwrap_or_else(|_| r#"{"error":"serialization_failed"}"#.to_string()),
|
||||
@@ -73,6 +78,7 @@ impl LogMsg {
|
||||
EV_JSON_PATCH.len() + json_len + OVERHEAD
|
||||
}
|
||||
LogMsg::SessionId(s) => EV_SESSION_ID.len() + s.len() + OVERHEAD,
|
||||
LogMsg::Ready => EV_READY.len() + OVERHEAD,
|
||||
LogMsg::Finished => EV_FINISHED.len() + OVERHEAD,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
const i18nCheck = process.env.LINT_I18N === 'true';
|
||||
|
||||
// Presentational components - these must be stateless and receive all data via props
|
||||
const presentationalComponentPatterns = [
|
||||
'src/components/ui-new/views/**/*.tsx',
|
||||
'src/components/ui-new/primitives/**/*.tsx',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
@@ -16,7 +22,7 @@ module.exports = {
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh', '@typescript-eslint', 'unused-imports', 'i18next', 'eslint-comments', 'check-file'],
|
||||
plugins: ['react-refresh', '@typescript-eslint', 'unused-imports', 'i18next', 'eslint-comments', 'check-file', 'deprecation'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
@@ -168,12 +174,138 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
// Allow NiceModal usage in lib/modals.ts, App.tsx (for Provider), and dialog component files
|
||||
files: ['src/lib/modals.ts', 'src/App.tsx', 'src/components/dialogs/**/*.{ts,tsx}'],
|
||||
// Allow NiceModal usage in lib/modals.ts, design scope files (for Provider), and dialog component files
|
||||
files: [
|
||||
'src/lib/modals.ts',
|
||||
'src/components/legacy-design/LegacyDesignScope.tsx',
|
||||
'src/components/ui-new/scope/NewDesignScope.tsx',
|
||||
'src/components/dialogs/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// ui-new components must use Phosphor icons (not Lucide) and avoid deprecated APIs
|
||||
files: ['src/components/ui-new/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'deprecation/deprecation': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'lucide-react',
|
||||
message: 'Use @phosphor-icons/react instead of lucide-react in ui-new components.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// Icon size restrictions - use Tailwind design system sizes
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'JSXAttribute[name.name="size"][value.type="JSXExpressionContainer"]',
|
||||
message:
|
||||
'Icons should use Tailwind size classes (size-icon-xs, size-icon-sm, size-icon-base, size-icon-lg, size-icon-xl) instead of the size prop. Example: <Icon className="size-icon-base" />',
|
||||
},
|
||||
{
|
||||
// Catch arbitrary pixel sizes like size-[10px], size-[7px], etc. in className
|
||||
selector: 'Literal[value=/size-\\[\\d+px\\]/]',
|
||||
message:
|
||||
'Use standard icon sizes (size-icon-xs, size-icon-sm, size-icon-base, size-icon-lg, size-icon-xl) instead of arbitrary pixel values like size-[Npx].',
|
||||
},
|
||||
{
|
||||
// Catch generic tailwind sizes like size-1, size-3, size-1.5, etc. (not size-icon-* or size-dot)
|
||||
selector: 'Literal[value=/(?<!icon-)(?<!-)size-[0-9]/]',
|
||||
message:
|
||||
'Use design system sizes (size-icon-xs, size-icon-sm, size-icon-base, size-icon-lg, size-icon-xl, size-dot) instead of generic Tailwind sizes.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Logic hooks in ui-new/hooks/ - no JSX allowed
|
||||
files: ['src/components/ui-new/hooks/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'JSXElement',
|
||||
message: 'Logic hooks must not contain JSX. Return data and callbacks only.',
|
||||
},
|
||||
{
|
||||
selector: 'JSXFragment',
|
||||
message: 'Logic hooks must not contain JSX fragments.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Presentational components (views & primitives) - strict presentation rules (no logic)
|
||||
files: presentationalComponentPatterns,
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@/lib/api',
|
||||
message: 'Presentational components cannot import API. Pass data via props.',
|
||||
},
|
||||
{
|
||||
name: '@tanstack/react-query',
|
||||
importNames: ['useQuery', 'useMutation', 'useQueryClient', 'useInfiniteQuery'],
|
||||
message: 'Presentational components cannot use data fetching hooks. Pass data via props.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useState"]',
|
||||
message: 'Presentational components should not manage state. Use controlled props.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useReducer"]',
|
||||
message: 'Presentational components should not use useReducer. Use container component.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useContext"]',
|
||||
message: 'Presentational components should not consume context. Pass data via props.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useQuery"]',
|
||||
message: 'Presentational components should not fetch data. Pass data via props.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useMutation"]',
|
||||
message: 'Presentational components should not mutate data. Pass callbacks via props.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useInfiniteQuery"]',
|
||||
message: 'Presentational components should not fetch data. Pass data via props.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useEffect"]',
|
||||
message: 'Presentational components should avoid side effects. Move to container.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useLayoutEffect"]',
|
||||
message: 'Presentational components should avoid layout effects. Move to container.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useCallback"]',
|
||||
message: 'Presentational components should receive callbacks via props.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="useNavigate"]',
|
||||
message: 'Presentational components should not handle navigation. Pass callbacks via props.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
72
frontend/CLAUDE.md
Normal file
@@ -0,0 +1,72 @@
|
||||
## New Design System Styling Guidelines
|
||||
|
||||
### CSS Variables & Tailwind Config
|
||||
|
||||
The new design uses custom CSS variables defined in `src/styles/new/index.css` and configured in `tailwind.new.config.js`. All styles are scoped to the `.new-design` class.
|
||||
|
||||
### Colors
|
||||
|
||||
**Text colors** (use these instead of `text-gray-*`):
|
||||
- `text-high` - Primary text, highest contrast
|
||||
- `text-normal` - Standard text
|
||||
- `text-low` - Muted/secondary text, placeholders
|
||||
|
||||
**Background colors**:
|
||||
- `bg-primary` - Main background
|
||||
- `bg-secondary` - Slightly darker, used for inputs, cards, sidebars
|
||||
- `bg-panel` - Panel/elevated surfaces
|
||||
|
||||
**Accent colors**:
|
||||
- `brand` - Orange accent (`hsl(25 82% 54%)`)
|
||||
- `error` - Error states
|
||||
- `success` - Success states
|
||||
|
||||
### Typography
|
||||
|
||||
**Font families**:
|
||||
- `font-ibm-plex-sans` - Default sans-serif
|
||||
- `font-ibm-plex-mono` - Monospace/code
|
||||
|
||||
**Font sizes** (smaller than typical Tailwind defaults):
|
||||
- `text-xs` - 8px
|
||||
- `text-sm` - 10px
|
||||
- `text-base` - 12px (default)
|
||||
- `text-lg` - 14px
|
||||
- `text-xl` - 16px
|
||||
|
||||
### Spacing
|
||||
|
||||
Custom spacing tokens:
|
||||
- `p-half` / `m-half` - 6px
|
||||
- `p-base` / `m-base` - 12px
|
||||
- `p-double` / `m-double` - 24px
|
||||
|
||||
### Border Radius
|
||||
|
||||
Uses a small radius by default (`--radius: 0.125rem`):
|
||||
- `rounded` - Default small radius
|
||||
- `rounded-sm`, `rounded-md`, `rounded-lg` - Progressively larger
|
||||
|
||||
### Focus States
|
||||
|
||||
Focus rings use `ring-brand` (orange) and are inset by default.
|
||||
|
||||
### Example Component Styling
|
||||
|
||||
```tsx
|
||||
// Input field
|
||||
className="px-base bg-secondary rounded border text-base text-normal placeholder:text-low focus:outline-none focus:ring-1 focus:ring-brand"
|
||||
|
||||
// Button (icon)
|
||||
className="flex items-center justify-center bg-secondary rounded border text-low hover:text-normal"
|
||||
|
||||
// Sidebar container
|
||||
className="w-64 bg-secondary shrink-0 p-base"
|
||||
```
|
||||
|
||||
### Architecture Rules
|
||||
|
||||
- **View components** (in `views/`) should be stateless - receive all data via props
|
||||
- **Container components** (in `containers/`) manage state and pass to views
|
||||
- **UI components** (in `ui-new/`) are reusable primitives
|
||||
- File names in `ui-new/` must be **PascalCase** (e.g., `Field.tsx`, `Label.tsx`)
|
||||
@@ -4,14 +4,15 @@
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"config": "tailwind.new.config.js",
|
||||
"css": "src/styles/new/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"ui": "@/components/ui-new",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
frontend/components.legacy.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"outDir": "components/ui",
|
||||
"tailwind": {
|
||||
"config": "tailwind.legacy.config.js",
|
||||
"css": "src/styles/legacy/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,13 @@
|
||||
"@lexical/markdown": "^0.36.2",
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lexical/rich-text": "^0.36.2",
|
||||
"@lexical/table": "^0.36.2",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
@@ -46,14 +50,18 @@
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@uiw/react-codemirror": "^4.25.1",
|
||||
"@virtuoso.dev/message-list": "^1.13.3",
|
||||
"allotment": "^1.20.5",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"click-to-react-component": "^1.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"developer-icons": "^6.0.4",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"framer-motion": "^12.23.24",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immer": "^11.1.3",
|
||||
"lexical": "^0.36.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.539.0",
|
||||
@@ -63,12 +71,13 @@
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hotkeys-hook": "^5.1.0",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-resizable-panels": "^4.0.13",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"rfc6902": "^5.1.2",
|
||||
"simple-icons": "^15.16.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vibe-kanban-web-companion": "^0.0.4",
|
||||
"wa-sqlite": "^1.0.0",
|
||||
@@ -89,6 +98,7 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-check-file": "^2.8.0",
|
||||
"eslint-plugin-deprecation": "^3.0.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-i18next": "^6.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
@@ -101,4 +111,4 @@
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
frontend/postcss.config.cjs
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
// No config specified - @config directives in CSS files take precedence
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
13
frontend/public/agents/amp-dark.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4892)">
|
||||
<path d="M7.97132 7.86071L9.20771 12.4879L8.08643 12.7875L7.02304 8.80868L3.04283 7.74664L3.34208 6.625L7.97132 7.86071Z" fill="white"/>
|
||||
<path d="M7.90682 8.74622L4.74346 11.8949L3.92455 11.0722L7.0879 7.92383L7.90682 8.74622Z" fill="white"/>
|
||||
<path d="M9.76378 6.06529L11.0002 10.6925L9.87889 10.9921L8.81546 7.01324L4.83528 5.95124L5.13453 4.82959L9.76378 6.06529Z" fill="white"/>
|
||||
<path d="M11.5644 4.26988L12.8008 8.89705L11.6795 9.19662L10.6161 5.21784L6.63589 4.15583L6.93511 3.03418L11.5644 4.26988Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4892">
|
||||
<rect width="10" height="10" fill="white" transform="translate(3 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 797 B |
13
frontend/public/agents/amp-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4952)">
|
||||
<path d="M7.97132 7.86071L9.20771 12.4879L8.08643 12.7875L7.02304 8.80868L3.04283 7.74664L3.34208 6.625L7.97132 7.86071Z" fill="#0C0C0C"/>
|
||||
<path d="M7.90682 8.74622L4.74346 11.8949L3.92455 11.0722L7.0879 7.92383L7.90682 8.74622Z" fill="#0C0C0C"/>
|
||||
<path d="M9.76378 6.06529L11.0002 10.6925L9.87889 10.9921L8.81546 7.01324L4.83528 5.95124L5.13453 4.82959L9.76378 6.06529Z" fill="#0C0C0C"/>
|
||||
<path d="M11.5644 4.26988L12.8008 8.89705L11.6795 9.19662L10.6161 5.21784L6.63589 4.15583L6.93511 3.03418L11.5644 4.26988Z" fill="#0C0C0C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4952">
|
||||
<rect width="10" height="10" fill="white" transform="translate(3 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 805 B |
3
frontend/public/agents/claude-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.96203 9.64812L6.92911 8.54516L6.96203 8.44903L6.92911 8.3959H6.83291L6.5038 8.37566L5.37975 8.34531L4.40506 8.30483L3.46076 8.25424L3.22278 8.20364L3 7.91019L3.02278 7.76347L3.22278 7.6294L3.50886 7.65469L4.14177 7.6977L5.09114 7.76347L5.77975 7.80395L6.8 7.91019H6.96203L6.98481 7.84442L6.92911 7.80395L6.88608 7.76347L5.9038 7.09815L4.84051 6.39489L4.28354 5.99013L3.98228 5.78523L3.83038 5.59297L3.76456 5.17303L4.03797 4.872L4.40506 4.89729L4.49873 4.92259L4.87089 5.20845L5.66582 5.82317L6.7038 6.58715L6.8557 6.71364L6.91646 6.67063L6.92405 6.64027L6.8557 6.52644L6.29114 5.50696L5.68861 4.46977L5.42025 4.03972L5.34937 3.78168C5.32405 3.67544 5.30633 3.5869 5.30633 3.47812L5.61772 3.05565L5.78987 3L6.20506 3.05565L6.37975 3.20744L6.63797 3.79686L7.0557 4.72527L7.7038 5.9876L7.89367 6.362L7.99494 6.70858L8.03291 6.81482H8.09873V6.75411L8.1519 6.04326L8.25063 5.1705L8.34684 4.04731L8.37975 3.73109L8.53671 3.35163L8.8481 3.14672L9.09114 3.26309L9.29114 3.54895L9.26329 3.73362L9.1443 4.50519L8.91139 5.71439L8.75949 6.52391H8.8481L8.94937 6.42272L9.35949 5.87883L10.0481 5.01872L10.3519 4.67721L10.7063 4.30028L10.9342 4.12067H11.3646L11.681 4.5912L11.5392 5.0769L11.0962 5.6385L10.7291 6.11409L10.2025 6.82241L9.87342 7.38907L9.9038 7.43461L9.98228 7.42702L11.1722 7.17405L11.8152 7.05768L12.5823 6.92613L12.9291 7.08803L12.9671 7.25247L12.8304 7.58892L12.0101 7.7913L11.0481 7.98356L9.61519 8.32254L9.59747 8.33519L9.61772 8.36049L10.2633 8.4212L10.5392 8.43638H11.2152L12.4734 8.52998L12.8025 8.74753L13 9.01315L12.9671 9.21553L12.4608 9.47356L11.7772 9.31166L10.1823 8.9322L9.63544 8.7956H9.55949V8.84113L10.0152 9.28637L10.8506 10.0402L11.8962 11.0116L11.9494 11.252L11.8152 11.4417L11.6734 11.4215L10.7544 10.7308L10.4 10.4197L9.59747 9.74425H9.5443V9.81508L9.72911 10.0858L10.7063 11.553L10.757 12.0033L10.6861 12.15L10.4329 12.2386L10.1544 12.188L9.58228 11.386L8.99241 10.4829L8.51646 9.67341L8.45823 9.7063L8.17722 12.7293L8.04557 12.8836L7.74177 13L7.48861 12.8077L7.35443 12.4966L7.48861 11.8819L7.65063 11.0799L7.78228 10.4424L7.90127 9.65065L7.97215 9.38755L7.96709 9.36985L7.90886 9.37744L7.31139 10.1971L6.40253 11.424L5.68354 12.193L5.51139 12.2613L5.21266 12.107L5.24051 11.8313L5.40759 11.5859L6.40253 10.321L7.00253 9.53681L7.38987 9.08399L7.38734 9.01821H7.36456L4.72152 10.7334L4.25063 10.7941L4.0481 10.6044L4.07342 10.2932L4.16962 10.192L4.96456 9.64559L4.96203 9.64812Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
3
frontend/public/agents/claude-light.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.96203 9.64812L6.92911 8.54516L6.96203 8.44903L6.92911 8.3959H6.83291L6.5038 8.37566L5.37975 8.34531L4.40506 8.30483L3.46076 8.25424L3.22278 8.20364L3 7.91019L3.02278 7.76347L3.22278 7.6294L3.50886 7.65469L4.14177 7.6977L5.09114 7.76347L5.77975 7.80395L6.8 7.91019H6.96203L6.98481 7.84442L6.92911 7.80395L6.88608 7.76347L5.9038 7.09815L4.84051 6.39489L4.28354 5.99013L3.98228 5.78523L3.83038 5.59297L3.76456 5.17303L4.03797 4.872L4.40506 4.89729L4.49873 4.92259L4.87089 5.20845L5.66582 5.82317L6.7038 6.58715L6.8557 6.71364L6.91646 6.67063L6.92405 6.64027L6.8557 6.52644L6.29114 5.50696L5.68861 4.46977L5.42025 4.03972L5.34937 3.78168C5.32405 3.67544 5.30633 3.5869 5.30633 3.47812L5.61772 3.05565L5.78987 3L6.20506 3.05565L6.37975 3.20744L6.63797 3.79686L7.0557 4.72527L7.7038 5.9876L7.89367 6.362L7.99494 6.70858L8.03291 6.81482H8.09873V6.75411L8.1519 6.04326L8.25063 5.1705L8.34684 4.04731L8.37975 3.73109L8.53671 3.35163L8.8481 3.14672L9.09114 3.26309L9.29114 3.54895L9.26329 3.73362L9.1443 4.50519L8.91139 5.71439L8.75949 6.52391H8.8481L8.94937 6.42272L9.35949 5.87883L10.0481 5.01872L10.3519 4.67721L10.7063 4.30028L10.9342 4.12067H11.3646L11.681 4.5912L11.5392 5.0769L11.0962 5.6385L10.7291 6.11409L10.2025 6.82241L9.87342 7.38907L9.9038 7.43461L9.98228 7.42702L11.1722 7.17405L11.8152 7.05768L12.5823 6.92613L12.9291 7.08803L12.9671 7.25247L12.8304 7.58892L12.0101 7.7913L11.0481 7.98356L9.61519 8.32254L9.59747 8.33519L9.61772 8.36049L10.2633 8.4212L10.5392 8.43638H11.2152L12.4734 8.52998L12.8025 8.74753L13 9.01315L12.9671 9.21553L12.4608 9.47356L11.7772 9.31166L10.1823 8.9322L9.63544 8.7956H9.55949V8.84113L10.0152 9.28637L10.8506 10.0402L11.8962 11.0116L11.9494 11.252L11.8152 11.4417L11.6734 11.4215L10.7544 10.7308L10.4 10.4197L9.59747 9.74425H9.5443V9.81508L9.72911 10.0858L10.7063 11.553L10.757 12.0033L10.6861 12.15L10.4329 12.2386L10.1544 12.188L9.58228 11.386L8.99241 10.4829L8.51646 9.67341L8.45823 9.7063L8.17722 12.7293L8.04557 12.8836L7.74177 13L7.48861 12.8077L7.35443 12.4966L7.48861 11.8819L7.65063 11.0799L7.78228 10.4424L7.90127 9.65065L7.97215 9.38755L7.96709 9.36985L7.90886 9.37744L7.31139 10.1971L6.40253 11.424L5.68354 12.193L5.51139 12.2613L5.21266 12.107L5.24051 11.8313L5.40759 11.5859L6.40253 10.321L7.00253 9.53681L7.38987 9.08399L7.38734 9.01821H7.36456L4.72152 10.7334L4.25063 10.7941L4.0481 10.6044L4.07342 10.2932L4.16962 10.192L4.96456 9.64559L4.96203 9.64812Z" fill="#0C0C0C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
13
frontend/public/agents/codex-dark.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_2725_10692" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<path d="M15.9778 0H0V15.9778H15.9778V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2725_10692)">
|
||||
<mask id="mask1_2725_10692" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="2" width="10" height="11">
|
||||
<path d="M13 2.98877H3V12.8995H13V2.98877Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2725_10692)">
|
||||
<path d="M6.83545 6.59627V5.65474C6.83545 5.57544 6.86521 5.51596 6.93455 5.47634L8.82756 4.38618C9.08524 4.23752 9.39248 4.16818 9.70958 4.16818C10.8988 4.16818 11.6521 5.0899 11.6521 6.07102C11.6521 6.14038 11.6521 6.21968 11.6422 6.29898L9.67984 5.14931C9.56093 5.07995 9.44195 5.07995 9.32304 5.14931L6.83545 6.59627ZM11.2556 10.2633V8.01348C11.2556 7.87468 11.1961 7.7756 11.0772 7.70624L8.58965 6.25928L9.40233 5.79344C9.47169 5.75384 9.53118 5.75384 9.60054 5.79344L11.4935 6.88362C12.0387 7.2008 12.4053 7.87468 12.4053 8.52877C12.4053 9.28196 11.9594 9.97573 11.2556 10.2632V10.2633ZM6.25072 8.28113L5.43804 7.80544C5.3687 7.76583 5.33894 7.70634 5.33894 7.62704V5.4467C5.33894 4.38626 6.15162 3.58343 7.25173 3.58343C7.66804 3.58343 8.05448 3.72224 8.38162 3.96998L6.4292 5.09986C6.31031 5.1692 6.25082 5.2683 6.25082 5.40708V8.28122L6.25072 8.28113ZM7.99999 9.292L6.83545 8.63791V7.25046L7.99999 6.59637L9.16445 7.25046V8.63791L7.99999 9.292ZM8.74825 12.3049C8.33196 12.3049 7.94552 12.1661 7.61838 11.9184L9.57078 10.7885C9.68969 10.7192 9.74918 10.6201 9.74918 10.4813V7.60715L10.5718 8.08284C10.6412 8.12244 10.6709 8.18193 10.6709 8.26122V10.4416C10.6709 11.502 9.84828 12.3048 8.74825 12.3048V12.3049ZM6.39937 10.0948L4.50636 9.00465C3.96123 8.68746 3.59458 8.01359 3.59458 7.3595C3.59458 6.59637 4.05048 5.91254 4.7541 5.6251V7.88474C4.7541 8.02352 4.81361 8.12263 4.9325 8.19197L7.41024 9.62899L6.59756 10.0948C6.52822 10.1344 6.46871 10.1344 6.39937 10.0948ZM6.29042 11.7202C5.17049 11.7202 4.34788 10.8778 4.34788 9.83713C4.34788 9.75784 4.35781 9.67854 4.36766 9.59925L6.32008 10.7291C6.43897 10.7985 6.55797 10.7985 6.67686 10.7291L9.16445 9.2921V10.2336C9.16445 10.3129 9.1347 10.3724 9.06534 10.412L7.17236 11.5022C6.91466 11.6508 6.60741 11.7202 6.29032 11.7202H6.29042ZM8.74825 12.8995C9.94747 12.8995 10.9484 12.0472 11.1764 10.9174C12.2864 10.6299 13 9.58929 13 8.52887C13 7.83508 12.7027 7.16121 12.1675 6.67556C12.2171 6.46742 12.2468 6.25928 12.2468 6.05124C12.2468 4.63402 11.0971 3.5735 9.76907 3.5735C9.50154 3.5735 9.24385 3.61309 8.98615 3.70235C8.5401 3.26625 7.92563 2.98877 7.25173 2.98877C6.05253 2.98877 5.0516 3.84105 4.82357 4.97091C3.71358 5.25834 3 6.29898 3 7.35939C3 8.05318 3.29729 8.72706 3.83249 9.21271C3.78294 9.42085 3.75319 9.62899 3.75319 9.83703C3.75319 11.2542 4.90286 12.3148 6.23091 12.3148C6.49846 12.3148 6.75615 12.2752 7.01385 12.1859C7.45979 12.622 8.07427 12.8995 8.74825 12.8995Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
13
frontend/public/agents/codex-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_2725_10672" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<path d="M15.9913 0.00244141H0.0134659V15.9803H15.9913V0.00244141Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2725_10672)">
|
||||
<mask id="mask1_2725_10672" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="10" height="10">
|
||||
<path d="M13 3H3V12.9107H13V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2725_10672)">
|
||||
<path d="M6.83545 6.6075V5.66597C6.83545 5.58667 6.86521 5.52719 6.93455 5.48759L8.82756 4.39741C9.08524 4.24875 9.39248 4.17941 9.70958 4.17941C10.8988 4.17941 11.6521 5.10113 11.6521 6.08225C11.6521 6.15162 11.6521 6.23091 11.6422 6.31021L9.67984 5.16054C9.56093 5.0912 9.44195 5.0912 9.32304 5.16054L6.83545 6.6075ZM11.2556 10.2745V8.02471C11.2556 7.88593 11.1961 7.78683 11.0772 7.71747L8.58965 6.27051L9.40233 5.80467C9.47169 5.76507 9.53118 5.76507 9.60054 5.80467L11.4935 6.89485C12.0387 7.21203 12.4053 7.88593 12.4053 8.54C12.4053 9.29319 11.9594 9.98698 11.2556 10.2744V10.2745ZM6.25072 8.29236L5.43804 7.81667C5.3687 7.77708 5.33894 7.71757 5.33894 7.63827V5.45793C5.33894 4.39751 6.15162 3.59468 7.25173 3.59468C7.66804 3.59468 8.05448 3.73347 8.38162 3.98123L6.4292 5.11109C6.31031 5.18043 6.25082 5.27953 6.25082 5.41833V8.29245L6.25072 8.29236ZM7.99999 9.30323L6.83545 8.64914V7.26169L7.99999 6.6076L9.16445 7.26169V8.64914L7.99999 9.30323ZM8.74825 12.3161C8.33196 12.3161 7.94552 12.1774 7.61838 11.9296L9.57078 10.7997C9.68969 10.7304 9.74918 10.6313 9.74918 10.4925V7.61838L10.5718 8.09407C10.6412 8.13367 10.6709 8.19316 10.6709 8.27247V10.4528C10.6709 11.5132 9.84828 12.3161 8.74825 12.3161V12.3161ZM6.39937 10.1061L4.50636 9.0159C3.96123 8.69869 3.59458 8.02482 3.59458 7.37073C3.59458 6.6076 4.05048 5.92377 4.7541 5.63633V7.89597C4.7541 8.03475 4.81361 8.13386 4.9325 8.20322L7.41024 9.64022L6.59756 10.1061C6.52822 10.1457 6.46871 10.1457 6.39937 10.1061ZM6.29042 11.7314C5.17049 11.7314 4.34788 10.889 4.34788 9.84836C4.34788 9.76907 4.35781 9.68977 4.36766 9.61048L6.32008 10.7403C6.43897 10.8097 6.55797 10.8097 6.67686 10.7403L9.16445 9.30333V10.2449C9.16445 10.3242 9.1347 10.3836 9.06534 10.4232L7.17236 11.5134C6.91466 11.6621 6.60741 11.7314 6.29032 11.7314H6.29042ZM8.74825 12.9107C9.94747 12.9107 10.9484 12.0585 11.1764 10.9286C12.2864 10.6412 13 9.60052 13 8.5401C13 7.84631 12.7027 7.17244 12.1675 6.68679C12.2171 6.47865 12.2468 6.27051 12.2468 6.06247C12.2468 4.64525 11.0971 3.58473 9.76907 3.58473C9.50154 3.58473 9.24385 3.62432 8.98615 3.71358C8.5401 3.27748 7.92563 3 7.25173 3C6.05253 3 5.0516 3.85228 4.82357 4.98214C3.71358 5.26958 3 6.31021 3 7.37062C3 8.06441 3.29729 8.73829 3.83249 9.22393C3.78294 9.43208 3.75319 9.64022 3.75319 9.84828C3.75319 11.2655 4.90286 12.326 6.23091 12.326C6.49846 12.326 6.75615 12.2864 7.01385 12.1972C7.45979 12.6332 8.07427 12.9107 8.74825 12.9107Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
3
frontend/public/agents/copilot-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0188 5.20589C11.5696 5.77853 11.8008 6.56 11.8979 7.65516C12.1571 7.65516 12.3979 7.712 12.5613 7.93053L12.8654 8.33642C12.9529 8.45347 13 8.59326 13 8.73853V9.84168C13 9.98442 12.9279 10.1234 12.8113 10.2072C11.4329 11.2008 9.73208 12 8 12C6.08333 12 4.16458 10.9124 3.18875 10.2072C3.07208 10.1229 3.00042 9.984 3 9.84168V8.73853C3 8.59326 3.04708 8.45263 3.13375 8.336L3.43792 7.93053C3.60125 7.71284 3.84375 7.65516 4.10167 7.65516L4.11375 7.53011C4.21792 6.50021 4.45125 5.75621 4.98125 5.20589C6.00667 4.13642 7.36042 4.00547 7.95875 4H8.04125C8.63958 4.00547 9.99333 4.136 11.0188 5.20589ZM8.00042 7.02821C7.88208 7.02821 7.745 7.03495 7.59958 7.04926C7.54833 7.23747 7.4725 7.40716 7.36208 7.51579C6.92458 7.94653 6.39708 8.01263 6.11458 8.01263C5.84875 8.01263 5.57042 7.95789 5.34333 7.81726C5.12833 7.88674 4.92167 7.98695 4.90833 8.23663C4.89081 8.64116 4.88206 9.04603 4.88208 9.45095L4.88125 9.65305C4.88042 9.8901 4.87917 10.1272 4.87583 10.3646C4.87667 10.5019 4.96083 10.6299 5.08833 10.6867C6.1225 11.1507 7.10083 11.3844 8.00083 11.3844C8.89917 11.3844 9.8775 11.1507 10.9112 10.6867C10.9734 10.6591 11.0265 10.614 11.0642 10.5569C11.1018 10.4997 11.1225 10.4329 11.1238 10.3642C11.1363 9.656 11.1263 8.94442 11.0921 8.23663C11.0792 7.98568 10.8721 7.88716 10.6562 7.81726C10.4287 7.95747 10.1512 8.01263 9.88542 8.01263C9.60333 8.01263 9.07625 7.94653 8.63833 7.51579C8.5275 7.40716 8.45208 7.23747 8.40083 7.04926C8.2675 7.03579 8.13375 7.02863 8.00042 7.02821ZM6.94833 8.71789C7.17292 8.71789 7.355 8.89726 7.355 9.11789V9.856C7.355 10.0771 7.17292 10.256 6.94833 10.256C6.84194 10.2573 6.73937 10.216 6.66313 10.141C6.58689 10.066 6.5432 9.96351 6.54167 9.856V9.11832C6.54167 8.89726 6.72375 8.71789 6.94833 8.71789ZM9.03167 8.71789C9.25625 8.71789 9.43833 8.89726 9.43833 9.11789V9.856C9.43833 10.0771 9.25625 10.256 9.03167 10.256C8.92528 10.2573 8.82271 10.216 8.74646 10.141C8.67022 10.066 8.62654 9.96351 8.625 9.856V9.11832C8.625 8.89726 8.80708 8.71789 9.03167 8.71789ZM6.18125 5.08926C5.74375 5.13221 5.375 5.27368 5.1875 5.47074C4.78125 5.90737 4.86875 7.01516 5.1 7.24926C5.26875 7.41516 5.5875 7.52589 5.93125 7.52589H5.96875C6.23917 7.52042 6.7125 7.45179 7.10625 7.05853C7.2875 6.88589 7.4 6.45516 7.3875 6.01853C7.375 5.66737 7.275 5.37853 7.125 5.25516C6.9625 5.11368 6.59375 5.05221 6.18125 5.08926ZM8.875 5.25516C8.725 5.37811 8.625 5.66779 8.6125 6.01853C8.6 6.45516 8.7125 6.88589 8.89375 7.05853C9.29708 7.46147 9.78375 7.52337 10.0504 7.52589H10.0688C10.4125 7.52589 10.7312 7.41516 10.9 7.24926C11.1312 7.01516 11.2188 5.90737 10.8125 5.47074C10.625 5.27368 10.2562 5.13221 9.81875 5.08926C9.40625 5.05221 9.0375 5.11368 8.875 5.25516ZM8 6.15368C7.9 6.15368 7.78125 6.16 7.65 6.17221C7.6625 6.23958 7.66875 6.31368 7.675 6.39368L7.67458 6.46063C7.67412 6.49579 7.67217 6.53091 7.66875 6.56589C7.7625 6.55663 7.84583 6.55453 7.92375 6.55411H8.07625C8.15417 6.55411 8.2375 6.55663 8.33125 6.56589C8.325 6.50442 8.325 6.44926 8.325 6.39368C8.33125 6.31368 8.3375 6.24 8.35 6.17221C8.23367 6.1607 8.11688 6.15452 8 6.15368Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
3
frontend/public/agents/copilot-light.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0188 5.20589C11.5696 5.77853 11.8008 6.56 11.8979 7.65516C12.1571 7.65516 12.3979 7.712 12.5613 7.93053L12.8654 8.33642C12.9529 8.45347 13 8.59326 13 8.73853V9.84168C13 9.98442 12.9279 10.1234 12.8113 10.2072C11.4329 11.2008 9.73208 12 8 12C6.08333 12 4.16458 10.9124 3.18875 10.2072C3.07208 10.1229 3.00042 9.984 3 9.84168V8.73853C3 8.59326 3.04708 8.45263 3.13375 8.336L3.43792 7.93053C3.60125 7.71284 3.84375 7.65516 4.10167 7.65516L4.11375 7.53011C4.21792 6.50021 4.45125 5.75621 4.98125 5.20589C6.00667 4.13642 7.36042 4.00547 7.95875 4H8.04125C8.63958 4.00547 9.99333 4.136 11.0188 5.20589ZM8.00042 7.02821C7.88208 7.02821 7.745 7.03495 7.59958 7.04926C7.54833 7.23747 7.4725 7.40716 7.36208 7.51579C6.92458 7.94653 6.39708 8.01263 6.11458 8.01263C5.84875 8.01263 5.57042 7.95789 5.34333 7.81726C5.12833 7.88674 4.92167 7.98695 4.90833 8.23663C4.89081 8.64116 4.88206 9.04603 4.88208 9.45095L4.88125 9.65305C4.88042 9.8901 4.87917 10.1272 4.87583 10.3646C4.87667 10.5019 4.96083 10.6299 5.08833 10.6867C6.1225 11.1507 7.10083 11.3844 8.00083 11.3844C8.89917 11.3844 9.8775 11.1507 10.9112 10.6867C10.9734 10.6591 11.0265 10.614 11.0642 10.5569C11.1018 10.4997 11.1225 10.4329 11.1238 10.3642C11.1363 9.656 11.1263 8.94442 11.0921 8.23663C11.0792 7.98568 10.8721 7.88716 10.6562 7.81726C10.4287 7.95747 10.1512 8.01263 9.88542 8.01263C9.60333 8.01263 9.07625 7.94653 8.63833 7.51579C8.5275 7.40716 8.45208 7.23747 8.40083 7.04926C8.2675 7.03579 8.13375 7.02863 8.00042 7.02821ZM6.94833 8.71789C7.17292 8.71789 7.355 8.89726 7.355 9.11789V9.856C7.355 10.0771 7.17292 10.256 6.94833 10.256C6.84194 10.2573 6.73937 10.216 6.66313 10.141C6.58689 10.066 6.5432 9.96351 6.54167 9.856V9.11832C6.54167 8.89726 6.72375 8.71789 6.94833 8.71789ZM9.03167 8.71789C9.25625 8.71789 9.43833 8.89726 9.43833 9.11789V9.856C9.43833 10.0771 9.25625 10.256 9.03167 10.256C8.92528 10.2573 8.82271 10.216 8.74646 10.141C8.67022 10.066 8.62654 9.96351 8.625 9.856V9.11832C8.625 8.89726 8.80708 8.71789 9.03167 8.71789ZM6.18125 5.08926C5.74375 5.13221 5.375 5.27368 5.1875 5.47074C4.78125 5.90737 4.86875 7.01516 5.1 7.24926C5.26875 7.41516 5.5875 7.52589 5.93125 7.52589H5.96875C6.23917 7.52042 6.7125 7.45179 7.10625 7.05853C7.2875 6.88589 7.4 6.45516 7.3875 6.01853C7.375 5.66737 7.275 5.37853 7.125 5.25516C6.9625 5.11368 6.59375 5.05221 6.18125 5.08926ZM8.875 5.25516C8.725 5.37811 8.625 5.66779 8.6125 6.01853C8.6 6.45516 8.7125 6.88589 8.89375 7.05853C9.29708 7.46147 9.78375 7.52337 10.0504 7.52589H10.0688C10.4125 7.52589 10.7312 7.41516 10.9 7.24926C11.1312 7.01516 11.2188 5.90737 10.8125 5.47074C10.625 5.27368 10.2562 5.13221 9.81875 5.08926C9.40625 5.05221 9.0375 5.11368 8.875 5.25516ZM8 6.15368C7.9 6.15368 7.78125 6.16 7.65 6.17221C7.6625 6.23958 7.66875 6.31368 7.675 6.39368L7.67458 6.46063C7.67412 6.49579 7.67217 6.53091 7.66875 6.56589C7.7625 6.55663 7.84583 6.55453 7.92375 6.55411H8.07625C8.15417 6.55411 8.2375 6.55663 8.33125 6.56589C8.325 6.50442 8.325 6.44926 8.325 6.39368C8.33125 6.31368 8.3375 6.24 8.35 6.17221C8.23367 6.1607 8.11688 6.15452 8 6.15368Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
10
frontend/public/agents/cursor-dark.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4923)">
|
||||
<path d="M12.3207 5.36694L8.21317 3.05568C8.08128 2.98144 7.91853 2.98144 7.78663 3.05568L3.67933 5.36694C3.56845 5.42934 3.5 5.54473 3.5 5.66971V10.3304C3.5 10.4554 3.56845 10.5708 3.67933 10.6332L7.78683 12.9444C7.91872 13.0187 8.08147 13.0187 8.21337 12.9444L12.3209 10.6332C12.4317 10.5708 12.5002 10.4554 12.5002 10.3304V5.66971C12.5002 5.54473 12.4317 5.42934 12.3209 5.36694H12.3207ZM12.0627 5.85652L8.09748 12.5501C8.07067 12.5952 7.9999 12.5768 7.9999 12.5246V8.14166C7.9999 8.05408 7.95189 7.97308 7.87398 7.9291L3.97957 5.73774C3.93329 5.71162 3.95219 5.64264 4.0058 5.64264H11.9362C12.0488 5.64264 12.1192 5.76161 12.0629 5.85671H12.0627V5.85652Z" fill="#EDECEC"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4923">
|
||||
<rect width="9" height="10" fill="white" transform="translate(3.5 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 954 B |
10
frontend/public/agents/cursor-light.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4972)">
|
||||
<path d="M12.3207 5.36694L8.21317 3.05568C8.08128 2.98144 7.91853 2.98144 7.78663 3.05568L3.67933 5.36694C3.56845 5.42934 3.5 5.54473 3.5 5.66971V10.3304C3.5 10.4554 3.56845 10.5708 3.67933 10.6332L7.78683 12.9444C7.91872 13.0187 8.08147 13.0187 8.21337 12.9444L12.3209 10.6332C12.4317 10.5708 12.5002 10.4554 12.5002 10.3304V5.66971C12.5002 5.54473 12.4317 5.42934 12.3209 5.36694H12.3207ZM12.0627 5.85652L8.09748 12.5501C8.07067 12.5952 7.9999 12.5768 7.9999 12.5246V8.14166C7.9999 8.05408 7.95189 7.97308 7.87398 7.9291L3.97957 5.73774C3.93329 5.71162 3.95219 5.64264 4.0058 5.64264H11.9362C12.0488 5.64264 12.1192 5.76161 12.0629 5.85671H12.0627V5.85652Z" fill="#0C0C0C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4972">
|
||||
<rect width="9" height="10" fill="white" transform="translate(3.5 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 954 B |
16
frontend/public/agents/droid-dark.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
16
frontend/public/agents/droid-light.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
13
frontend/public/agents/gemini-dark.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4926)">
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#F5F5F5"/>
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#F5F5F5"/>
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#F5F5F5"/>
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#F5F5F5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4926">
|
||||
<rect width="10" height="10" fill="white" transform="translate(3 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
13
frontend/public/agents/gemini-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4975)">
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#0C0C0C"/>
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#0C0C0C"/>
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#0C0C0C"/>
|
||||
<path d="M11.59 7.51467C10.8978 7.22001 10.2684 6.79556 9.73584 6.26426C8.99412 5.52106 8.46496 4.5928 8.20334 3.57592C8.19186 3.53076 8.16565 3.49071 8.12887 3.46211C8.09208 3.4335 8.04681 3.41797 8.00021 3.41797C7.95361 3.41797 7.90834 3.4335 7.87156 3.46211C7.83477 3.49071 7.80857 3.53076 7.79709 3.57592C7.53491 4.59267 7.00566 5.52082 6.26417 6.26426C5.73152 6.7955 5.10215 7.21993 4.41 7.51467C4.13917 7.63134 3.86084 7.72509 3.57584 7.79717C3.5304 7.80834 3.49001 7.83442 3.46114 7.87124C3.43227 7.90807 3.41658 7.95351 3.41658 8.0003C3.41658 8.04709 3.43227 8.09253 3.46114 8.12936C3.49001 8.16618 3.5304 8.19226 3.57584 8.20342C3.86084 8.27509 4.13834 8.36884 4.41 8.48551C5.10219 8.78018 5.73157 9.20462 6.26417 9.73592C7.00608 10.4792 7.53539 11.4076 7.79709 12.4247C7.80825 12.4701 7.83433 12.5105 7.87116 12.5394C7.90798 12.5682 7.95342 12.5839 8.00021 12.5839C8.047 12.5839 8.09245 12.5682 8.12927 12.5394C8.16609 12.5105 8.19217 12.4701 8.20334 12.4247C8.275 12.1393 8.36875 11.8618 8.48542 11.5901C8.78007 10.8979 9.20452 10.2685 9.73584 9.73592C10.4792 8.99418 11.4076 8.46502 12.4246 8.20342C12.4698 8.19195 12.5098 8.16574 12.5384 8.12896C12.567 8.09217 12.5825 8.0469 12.5825 8.0003C12.5825 7.9537 12.567 7.90843 12.5384 7.87164C12.5098 7.83486 12.4698 7.80865 12.4246 7.79717C12.1393 7.7254 11.8602 7.63093 11.59 7.51467Z" fill="#0C0C0C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4975">
|
||||
<rect width="10" height="10" fill="white" transform="translate(3 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
21
frontend/public/agents/opencode-dark.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4932)">
|
||||
<mask id="mask0_2163_4932" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="3" width="8" height="10">
|
||||
<path d="M12 3H4V13H12V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2163_4932)">
|
||||
<mask id="mask1_2163_4932" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="3" width="8" height="10">
|
||||
<path d="M12 3H4V13H12V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2163_4932)">
|
||||
<path d="M10 11H6V7H10V11Z" fill="#4B4646"/>
|
||||
<path d="M10 5H6V11H10V5ZM12 13H4V3H12V13Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4932">
|
||||
<rect width="8" height="10" fill="white" transform="translate(4 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 789 B |
21
frontend/public/agents/opencode-light.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2163_4981)">
|
||||
<mask id="mask0_2163_4981" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="3" width="8" height="10">
|
||||
<path d="M12 3H4V13H12V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2163_4981)">
|
||||
<mask id="mask1_2163_4981" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="3" width="8" height="10">
|
||||
<path d="M12 3H4V13H12V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2163_4981)">
|
||||
<path d="M10 11H6V7H10V11Z" fill="#4B4646"/>
|
||||
<path d="M10 5H6V11H10V5ZM12 13H4V3H12V13Z" fill="#0C0C0C"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2163_4981">
|
||||
<rect width="8" height="10" fill="white" transform="translate(4 3)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
3
frontend/public/agents/qwen-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.2747 3.15462C8.45333 3.46825 8.63106 3.7828 8.80833 4.09779C8.8155 4.11039 8.82588 4.12086 8.83842 4.12812C8.85096 4.13539 8.8652 4.1392 8.87969 4.13916H11.4033C11.4824 4.13916 11.5497 4.18915 11.606 4.28779L12.2669 5.45596C12.3533 5.60914 12.376 5.67323 12.2778 5.83641C12.1597 6.03186 12.0447 6.22914 11.9324 6.42732L11.7656 6.7264C11.7174 6.81549 11.6642 6.85368 11.7474 6.95913L12.9528 9.06684C13.031 9.20365 13.0033 9.29138 12.9333 9.41683C12.7346 9.77365 12.5324 10.1277 12.3265 10.4805C12.2542 10.6041 12.1665 10.6509 12.0174 10.6486C11.6642 10.6414 11.3119 10.6441 10.9597 10.6559C10.9521 10.6563 10.9448 10.6586 10.9383 10.6626C10.9319 10.6665 10.9266 10.6721 10.9228 10.6786C10.5164 11.3988 10.1066 12.1169 9.69332 12.8332C9.6165 12.9663 9.52059 12.9982 9.36378 12.9986C8.9106 13 8.45379 13.0004 7.99243 12.9995C7.94948 12.9994 7.90732 12.988 7.8702 12.9663C7.83309 12.9447 7.80235 12.9137 7.78107 12.8763L7.17426 11.8204C7.17073 11.8135 7.16531 11.8078 7.15863 11.8038C7.15195 11.7999 7.14429 11.7979 7.13653 11.7982H4.8102C4.68065 11.8118 4.55884 11.7977 4.44429 11.7564L3.71567 10.4973C3.69412 10.46 3.6827 10.4177 3.68254 10.3747C3.68238 10.3316 3.69349 10.2893 3.71476 10.2518L4.26339 9.2882C4.2712 9.27457 4.27531 9.25914 4.27531 9.24343C4.27531 9.22772 4.2712 9.21228 4.26339 9.19865C3.9776 8.70387 3.69352 8.20812 3.41113 7.71139L3.05204 7.07731C2.97931 6.9364 2.9734 6.85186 3.09522 6.63868C3.30658 6.26914 3.51658 5.90005 3.72567 5.53142C3.78567 5.42505 3.86385 5.3796 3.99112 5.37914C4.38338 5.37749 4.77565 5.37734 5.16792 5.37869C5.17783 5.37861 5.18754 5.37593 5.19608 5.3709C5.20462 5.36587 5.21168 5.35868 5.21656 5.35005L6.492 3.12507C6.51132 3.09123 6.53923 3.06307 6.57291 3.04344C6.60658 3.02381 6.64483 3.0134 6.68381 3.01326C6.92199 3.0128 7.16244 3.01326 7.40335 3.01053L7.86561 3.00008C8.02061 2.99871 8.1947 3.01462 8.2747 3.15462ZM6.71472 3.3378C6.70993 3.3378 6.70522 3.33905 6.70107 3.34145C6.69692 3.34384 6.69348 3.34729 6.69108 3.35144L5.38837 5.63096C5.38212 5.6417 5.37317 5.65062 5.3624 5.65684C5.35164 5.66306 5.33944 5.66636 5.32701 5.66641H4.0243C3.99885 5.66641 3.99248 5.67778 4.00566 5.70005L6.64654 10.3164C6.6579 10.3355 6.65245 10.3446 6.63108 10.345L5.36065 10.3518C5.34207 10.3512 5.3237 10.3558 5.30763 10.3651C5.29155 10.3745 5.27842 10.3881 5.26974 10.4046L4.66975 11.4545C4.64975 11.49 4.6602 11.5082 4.70065 11.5082L7.2988 11.5118C7.31971 11.5118 7.33517 11.5209 7.34608 11.5395L7.98379 12.655C8.0047 12.6918 8.02561 12.6923 8.04698 12.655L10.3224 8.6732L10.6783 8.04503C10.6805 8.04115 10.6836 8.03792 10.6875 8.03567C10.6913 8.03342 10.6957 8.03223 10.7001 8.03223C10.7046 8.03223 10.7089 8.03342 10.7128 8.03567C10.7166 8.03792 10.7198 8.04115 10.7219 8.04503L11.3692 9.19502C11.3741 9.20362 11.3811 9.21077 11.3897 9.21573C11.3982 9.22068 11.408 9.22326 11.4178 9.2232L12.6737 9.21411C12.677 9.21414 12.6801 9.21331 12.6829 9.21171C12.6857 9.21012 12.688 9.20781 12.6896 9.20502C12.6912 9.20224 12.692 9.19911 12.692 9.19593C12.692 9.19274 12.6912 9.18961 12.6896 9.18684L11.3715 6.87504C11.3667 6.86731 11.3642 6.85842 11.3642 6.84936C11.3642 6.84029 11.3667 6.8314 11.3715 6.82368L11.5047 6.59322L12.0137 5.6946C12.0247 5.67596 12.0192 5.66641 11.9978 5.66641H6.72745C6.70063 5.66641 6.69427 5.6546 6.7079 5.63141L7.35971 4.49279C7.3646 4.48503 7.36719 4.47605 7.36719 4.46688C7.36719 4.45771 7.3646 4.44873 7.35971 4.44097L6.73881 3.35189C6.73644 3.34759 6.73294 3.34401 6.7287 3.34153C6.72446 3.33905 6.71963 3.33776 6.71472 3.3378ZM9.57377 6.98322C9.59468 6.98322 9.60014 6.99231 9.58923 7.01049L9.21105 7.67639L8.02334 9.76047C8.02111 9.76452 8.01781 9.76789 8.01381 9.77022C8.0098 9.77254 8.00524 9.77373 8.00061 9.77365C7.996 9.77363 7.99148 9.7724 7.98749 9.77009C7.9835 9.76778 7.98019 9.76446 7.97789 9.76047L6.40836 7.01867C6.39927 7.00322 6.40381 6.99504 6.42109 6.99413L6.51927 6.98867L9.57468 6.98322H9.57377Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
frontend/public/agents/qwen-light.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.2747 3.15462C8.45333 3.46825 8.63106 3.7828 8.80833 4.09779C8.8155 4.11039 8.82588 4.12086 8.83842 4.12812C8.85096 4.13539 8.8652 4.1392 8.87969 4.13916H11.4033C11.4824 4.13916 11.5497 4.18915 11.606 4.28779L12.2669 5.45596C12.3533 5.60914 12.376 5.67323 12.2778 5.83641C12.1597 6.03186 12.0447 6.22914 11.9324 6.42732L11.7656 6.7264C11.7174 6.81549 11.6642 6.85368 11.7474 6.95913L12.9528 9.06684C13.031 9.20365 13.0033 9.29138 12.9333 9.41683C12.7346 9.77365 12.5324 10.1277 12.3265 10.4805C12.2542 10.6041 12.1665 10.6509 12.0174 10.6486C11.6642 10.6414 11.3119 10.6441 10.9597 10.6559C10.9521 10.6563 10.9448 10.6586 10.9383 10.6626C10.9319 10.6665 10.9266 10.6721 10.9228 10.6786C10.5164 11.3988 10.1066 12.1169 9.69332 12.8332C9.6165 12.9663 9.52059 12.9982 9.36378 12.9986C8.9106 13 8.45379 13.0004 7.99243 12.9995C7.94948 12.9994 7.90732 12.988 7.8702 12.9663C7.83309 12.9447 7.80235 12.9137 7.78107 12.8763L7.17426 11.8204C7.17073 11.8135 7.16531 11.8078 7.15863 11.8038C7.15195 11.7999 7.14429 11.7979 7.13653 11.7982H4.8102C4.68065 11.8118 4.55884 11.7977 4.44429 11.7564L3.71567 10.4973C3.69412 10.46 3.6827 10.4177 3.68254 10.3747C3.68238 10.3316 3.69349 10.2893 3.71476 10.2518L4.26339 9.2882C4.2712 9.27457 4.27531 9.25914 4.27531 9.24343C4.27531 9.22772 4.2712 9.21228 4.26339 9.19865C3.9776 8.70387 3.69352 8.20812 3.41113 7.71139L3.05204 7.07731C2.97931 6.9364 2.9734 6.85186 3.09522 6.63868C3.30658 6.26914 3.51658 5.90005 3.72567 5.53142C3.78567 5.42505 3.86385 5.3796 3.99112 5.37914C4.38338 5.37749 4.77565 5.37734 5.16792 5.37869C5.17783 5.37861 5.18754 5.37593 5.19608 5.3709C5.20462 5.36587 5.21168 5.35868 5.21656 5.35005L6.492 3.12507C6.51132 3.09123 6.53923 3.06307 6.57291 3.04344C6.60658 3.02381 6.64483 3.0134 6.68381 3.01326C6.92199 3.0128 7.16244 3.01326 7.40335 3.01053L7.86561 3.00008C8.02061 2.99871 8.1947 3.01462 8.2747 3.15462ZM6.71472 3.3378C6.70993 3.3378 6.70522 3.33905 6.70107 3.34145C6.69692 3.34384 6.69348 3.34729 6.69108 3.35144L5.38837 5.63096C5.38212 5.6417 5.37317 5.65062 5.3624 5.65684C5.35164 5.66306 5.33944 5.66636 5.32701 5.66641H4.0243C3.99885 5.66641 3.99248 5.67778 4.00566 5.70005L6.64654 10.3164C6.6579 10.3355 6.65245 10.3446 6.63108 10.345L5.36065 10.3518C5.34207 10.3512 5.3237 10.3558 5.30763 10.3651C5.29155 10.3745 5.27842 10.3881 5.26974 10.4046L4.66975 11.4545C4.64975 11.49 4.6602 11.5082 4.70065 11.5082L7.2988 11.5118C7.31971 11.5118 7.33517 11.5209 7.34608 11.5395L7.98379 12.655C8.0047 12.6918 8.02561 12.6923 8.04698 12.655L10.3224 8.6732L10.6783 8.04503C10.6805 8.04115 10.6836 8.03792 10.6875 8.03567C10.6913 8.03342 10.6957 8.03223 10.7001 8.03223C10.7046 8.03223 10.7089 8.03342 10.7128 8.03567C10.7166 8.03792 10.7198 8.04115 10.7219 8.04503L11.3692 9.19502C11.3741 9.20362 11.3811 9.21077 11.3897 9.21573C11.3982 9.22068 11.408 9.22326 11.4178 9.2232L12.6737 9.21411C12.677 9.21414 12.6801 9.21331 12.6829 9.21171C12.6857 9.21012 12.688 9.20781 12.6896 9.20502C12.6912 9.20224 12.692 9.19911 12.692 9.19593C12.692 9.19274 12.6912 9.18961 12.6896 9.18684L11.3715 6.87504C11.3667 6.86731 11.3642 6.85842 11.3642 6.84936C11.3642 6.84029 11.3667 6.8314 11.3715 6.82368L11.5047 6.59322L12.0137 5.6946C12.0247 5.67596 12.0192 5.66641 11.9978 5.66641H6.72745C6.70063 5.66641 6.69427 5.6546 6.7079 5.63141L7.35971 4.49279C7.3646 4.48503 7.36719 4.47605 7.36719 4.46688C7.36719 4.45771 7.3646 4.44873 7.35971 4.44097L6.73881 3.35189C6.73644 3.34759 6.73294 3.34401 6.7287 3.34153C6.72446 3.33905 6.71963 3.33776 6.71472 3.3378ZM9.57377 6.98322C9.59468 6.98322 9.60014 6.99231 9.58923 7.01049L9.21105 7.67639L8.02334 9.76047C8.02111 9.76452 8.01781 9.76789 8.01381 9.77022C8.0098 9.77254 8.00524 9.77373 8.00061 9.77365C7.996 9.77363 7.99148 9.7724 7.98749 9.77009C7.9835 9.76778 7.98019 9.76446 7.97789 9.76047L6.40836 7.01867C6.39927 7.00322 6.40381 6.99504 6.42109 6.99413L6.51927 6.98867L9.57468 6.98322H9.57377Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -6,6 +6,7 @@ import { Projects } from '@/pages/Projects';
|
||||
import { ProjectTasks } from '@/pages/ProjectTasks';
|
||||
import { FullAttemptLogsPage } from '@/pages/FullAttemptLogs';
|
||||
import { NormalLayout } from '@/components/layout/NormalLayout';
|
||||
import { NewDesignLayout } from '@/components/layout/NewDesignLayout';
|
||||
import { usePostHog } from 'posthog-js/react';
|
||||
import { useAuth } from '@/hooks';
|
||||
import { usePreviousPath } from '@/hooks/usePreviousPath';
|
||||
@@ -27,19 +28,24 @@ import { HotkeysProvider } from 'react-hotkeys-hook';
|
||||
import { ProjectProvider } from '@/contexts/ProjectContext';
|
||||
import { ThemeMode } from 'shared/types';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
|
||||
import { DisclaimerDialog } from '@/components/dialogs/global/DisclaimerDialog';
|
||||
import { OnboardingDialog } from '@/components/dialogs/global/OnboardingDialog';
|
||||
import { ReleaseNotesDialog } from '@/components/dialogs/global/ReleaseNotesDialog';
|
||||
import { ClickedElementsProvider } from './contexts/ClickedElementsProvider';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
|
||||
// Design scope components
|
||||
import { LegacyDesignScope } from '@/components/legacy-design/LegacyDesignScope';
|
||||
import { NewDesignScope } from '@/components/ui-new/scope/NewDesignScope';
|
||||
|
||||
// New design pages
|
||||
import { Workspaces } from '@/pages/ui-new/Workspaces';
|
||||
import { WorkspacesLanding } from '@/pages/ui-new/WorkspacesLanding';
|
||||
|
||||
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||
|
||||
function AppContent() {
|
||||
const { config, analyticsUserId, updateAndSaveConfig, loading } =
|
||||
useUserSystem();
|
||||
const { config, analyticsUserId, updateAndSaveConfig } = useUserSystem();
|
||||
const posthog = usePostHog();
|
||||
const { isSignedIn } = useAuth();
|
||||
|
||||
@@ -107,60 +113,84 @@ function AppContent() {
|
||||
};
|
||||
}, [config, isSignedIn, updateAndSaveConfig]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Loader message="Loading..." size={32} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// TODO: Disabled while developing FE only
|
||||
// if (loading) {
|
||||
// return (
|
||||
// <div className="min-h-screen bg-background flex items-center justify-center">
|
||||
// <Loader message="Loading..." size={32} />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider initialTheme={config?.theme || ThemeMode.SYSTEM}>
|
||||
<SearchProvider>
|
||||
<div className="h-screen flex flex-col bg-background">
|
||||
<SentryRoutes>
|
||||
{/* VS Code full-page logs route (outside NormalLayout for minimal UI) */}
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/full"
|
||||
element={<FullAttemptLogsPage />}
|
||||
/>
|
||||
<SentryRoutes>
|
||||
{/* ========== LEGACY DESIGN ROUTES ========== */}
|
||||
{/* VS Code full-page logs route (outside NormalLayout for minimal UI) */}
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/full"
|
||||
element={
|
||||
<LegacyDesignScope>
|
||||
<FullAttemptLogsPage />
|
||||
</LegacyDesignScope>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route element={<NormalLayout />}>
|
||||
<Route path="/" element={<Projects />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/projects/:projectId" element={<Projects />} />
|
||||
<Route
|
||||
element={
|
||||
<LegacyDesignScope>
|
||||
<NormalLayout />
|
||||
</LegacyDesignScope>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<Projects />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/projects/:projectId" element={<Projects />} />
|
||||
<Route
|
||||
path="/projects/:projectId/tasks"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route path="/settings/*" element={<SettingsLayout />}>
|
||||
<Route index element={<Navigate to="general" replace />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="projects" element={<ProjectSettings />} />
|
||||
<Route
|
||||
path="/projects/:projectId/tasks"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route path="/settings/*" element={<SettingsLayout />}>
|
||||
<Route index element={<Navigate to="general" replace />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="projects" element={<ProjectSettings />} />
|
||||
<Route
|
||||
path="organizations"
|
||||
element={<OrganizationSettings />}
|
||||
/>
|
||||
<Route path="agents" element={<AgentSettings />} />
|
||||
<Route path="mcp" element={<McpSettings />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/mcp-servers"
|
||||
element={<Navigate to="/settings/mcp" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId"
|
||||
element={<ProjectTasks />}
|
||||
path="organizations"
|
||||
element={<OrganizationSettings />}
|
||||
/>
|
||||
<Route path="agents" element={<AgentSettings />} />
|
||||
<Route path="mcp" element={<McpSettings />} />
|
||||
</Route>
|
||||
</SentryRoutes>
|
||||
</div>
|
||||
<Route
|
||||
path="/mcp-servers"
|
||||
element={<Navigate to="/settings/mcp" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* ========== NEW DESIGN ROUTES ========== */}
|
||||
<Route
|
||||
path="/workspaces"
|
||||
element={
|
||||
<NewDesignScope>
|
||||
<NewDesignLayout />
|
||||
</NewDesignScope>
|
||||
}
|
||||
>
|
||||
<Route index element={<WorkspacesLanding />} />
|
||||
<Route path="create" element={<Workspaces />} />
|
||||
<Route path=":workspaceId" element={<Workspaces />} />
|
||||
</Route>
|
||||
</SentryRoutes>
|
||||
</SearchProvider>
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
@@ -174,9 +204,7 @@ function App() {
|
||||
<ClickedElementsProvider>
|
||||
<ProjectProvider>
|
||||
<HotkeysProvider initiallyActiveScopes={['*', 'global', 'kanban']}>
|
||||
<NiceModal.Provider>
|
||||
<AppContent />
|
||||
</NiceModal.Provider>
|
||||
<AppContent />
|
||||
</HotkeysProvider>
|
||||
</ProjectProvider>
|
||||
</ClickedElementsProvider>
|
||||
|
||||
@@ -609,11 +609,7 @@ const getToolStatusAppearance = (status: ToolStatus): ToolStatusAppearance => {
|
||||
*******************/
|
||||
|
||||
export const DisplayConversationEntryMaxWidth = (props: Props) => {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[50rem]">
|
||||
<DisplayConversationEntry {...props} />
|
||||
</div>
|
||||
);
|
||||
return <DisplayConversationEntry {...props} />;
|
||||
};
|
||||
|
||||
function DisplayConversationEntry({
|
||||
|
||||
90
frontend/src/components/agents/AgentIcon.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { BaseCodingAgent, ThemeMode } from 'shared/types';
|
||||
import { useTheme } from '@/components/ThemeProvider';
|
||||
|
||||
type AgentIconProps = {
|
||||
agent: BaseCodingAgent | null | undefined;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function getResolvedTheme(theme: ThemeMode): 'light' | 'dark' {
|
||||
if (theme === ThemeMode.SYSTEM) {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
}
|
||||
return theme === ThemeMode.DARK ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function getAgentName(
|
||||
agent: BaseCodingAgent | null | undefined
|
||||
): string {
|
||||
if (!agent) return 'Agent';
|
||||
switch (agent) {
|
||||
case BaseCodingAgent.CLAUDE_CODE:
|
||||
return 'Claude Code';
|
||||
case BaseCodingAgent.AMP:
|
||||
return 'AMP';
|
||||
case BaseCodingAgent.GEMINI:
|
||||
return 'Gemini';
|
||||
case BaseCodingAgent.CODEX:
|
||||
return 'Codex';
|
||||
case BaseCodingAgent.OPENCODE:
|
||||
return 'OpenCode';
|
||||
case BaseCodingAgent.CURSOR_AGENT:
|
||||
return 'Cursor';
|
||||
case BaseCodingAgent.QWEN_CODE:
|
||||
return 'Qwen';
|
||||
case BaseCodingAgent.COPILOT:
|
||||
return 'Copilot';
|
||||
case BaseCodingAgent.DROID:
|
||||
return 'Droid';
|
||||
}
|
||||
}
|
||||
|
||||
export function AgentIcon({ agent, className = 'h-4 w-4' }: AgentIconProps) {
|
||||
const { theme } = useTheme();
|
||||
const resolvedTheme = getResolvedTheme(theme);
|
||||
const isDark = resolvedTheme === 'dark';
|
||||
const suffix = isDark ? '-dark' : '-light';
|
||||
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const agentName = getAgentName(agent);
|
||||
let iconPath = '';
|
||||
|
||||
switch (agent) {
|
||||
case BaseCodingAgent.CLAUDE_CODE:
|
||||
iconPath = `/agents/claude${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.AMP:
|
||||
iconPath = `/agents/amp${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.GEMINI:
|
||||
iconPath = `/agents/gemini${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.CODEX:
|
||||
iconPath = `/agents/codex${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.OPENCODE:
|
||||
iconPath = `/agents/opencode${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.CURSOR_AGENT:
|
||||
iconPath = `/agents/cursor${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.QWEN_CODE:
|
||||
iconPath = `/agents/qwen${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.COPILOT:
|
||||
iconPath = `/agents/copilot${suffix}.svg`;
|
||||
break;
|
||||
case BaseCodingAgent.DROID:
|
||||
iconPath = `/agents/droid${suffix}.svg`;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return <img src={iconPath} alt={agentName} className={className} />;
|
||||
}
|
||||
@@ -9,6 +9,8 @@ interface RawLogTextProps {
|
||||
as?: 'div' | 'span';
|
||||
className?: string;
|
||||
linkifyUrls?: boolean;
|
||||
searchQuery?: string;
|
||||
isCurrentMatch?: boolean;
|
||||
}
|
||||
|
||||
const RawLogText = memo(
|
||||
@@ -18,14 +20,43 @@ const RawLogText = memo(
|
||||
as: Component = 'div',
|
||||
className,
|
||||
linkifyUrls = false,
|
||||
searchQuery,
|
||||
isCurrentMatch = false,
|
||||
}: RawLogTextProps) => {
|
||||
// Only apply stderr fallback color when no ANSI codes are present
|
||||
const hasAnsiCodes = hasAnsi(content);
|
||||
const shouldApplyStderrFallback = channel === 'stderr' && !hasAnsiCodes;
|
||||
|
||||
const highlightClass = isCurrentMatch
|
||||
? 'bg-yellow-500/60 ring-1 ring-yellow-500 rounded-sm'
|
||||
: 'bg-yellow-500/30 rounded-sm';
|
||||
|
||||
const highlightMatches = (text: string, key: string | number) => {
|
||||
if (!searchQuery) {
|
||||
return <AnsiHtml key={key} text={text} />;
|
||||
}
|
||||
|
||||
const regex = new RegExp(
|
||||
`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`,
|
||||
'gi'
|
||||
);
|
||||
const parts = text.split(regex);
|
||||
|
||||
return parts.map((part, idx) => {
|
||||
if (part.toLowerCase() === searchQuery.toLowerCase()) {
|
||||
return (
|
||||
<mark key={`${key}-${idx}`} className={highlightClass}>
|
||||
<AnsiHtml text={part} />
|
||||
</mark>
|
||||
);
|
||||
}
|
||||
return <AnsiHtml key={`${key}-${idx}`} text={part} />;
|
||||
});
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!linkifyUrls) {
|
||||
return <AnsiHtml text={content} />;
|
||||
return highlightMatches(content, 'content');
|
||||
}
|
||||
|
||||
const urlRegex = /(https?:\/\/\S+)/g;
|
||||
@@ -46,8 +77,8 @@ const RawLogText = memo(
|
||||
</a>
|
||||
);
|
||||
}
|
||||
// For non-URL parts, apply ANSI formatting
|
||||
return <AnsiHtml key={index} text={part} />;
|
||||
// For non-URL parts, apply ANSI formatting with highlighting
|
||||
return highlightMatches(part, index);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { CreateProject } from 'shared/types';
|
||||
import { CreateProject, Project } from 'shared/types';
|
||||
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||
import { useProjectMutations } from '@/hooks/useProjectMutations';
|
||||
import { defineModal } from '@/lib/modals';
|
||||
@@ -16,14 +16,16 @@ import { RepoPickerDialog } from '@/components/dialogs/shared/RepoPickerDialog';
|
||||
|
||||
export interface ProjectFormDialogProps {}
|
||||
|
||||
export type ProjectFormDialogResult = 'saved' | 'canceled';
|
||||
export type ProjectFormDialogResult =
|
||||
| { status: 'saved'; project: Project }
|
||||
| { status: 'canceled' };
|
||||
|
||||
const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
|
||||
const modal = useModal();
|
||||
|
||||
const { createProject } = useProjectMutations({
|
||||
onCreateSuccess: () => {
|
||||
modal.resolve('saved' as ProjectFormDialogResult);
|
||||
onCreateSuccess: (project) => {
|
||||
modal.resolve({ status: 'saved', project } as ProjectFormDialogResult);
|
||||
modal.hide();
|
||||
},
|
||||
onCreateError: () => {},
|
||||
@@ -48,7 +50,7 @@ const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
|
||||
|
||||
createProjectMutate(createData);
|
||||
} else {
|
||||
modal.resolve('canceled' as ProjectFormDialogResult);
|
||||
modal.resolve({ status: 'canceled' } as ProjectFormDialogResult);
|
||||
modal.hide();
|
||||
}
|
||||
}, [createProjectMutate, modal]);
|
||||
@@ -66,7 +68,7 @@ const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
modal.resolve('canceled' as ProjectFormDialogResult);
|
||||
modal.resolve({ status: 'canceled' } as ProjectFormDialogResult);
|
||||
modal.hide();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,6 +93,7 @@ const FolderPickerDialogImpl = NiceModal.create<FolderPickerDialogProps>(
|
||||
|
||||
const handleFolderClick = (entry: DirectoryEntry) => {
|
||||
if (entry.is_directory) {
|
||||
setSearchTerm('');
|
||||
loadDirectory(entry.path);
|
||||
setManualPath(entry.path); // Auto-populate the manual path field
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ interface CreatePRDialogProps {
|
||||
targetBranch?: string;
|
||||
}
|
||||
|
||||
export type CreatePRDialogResult = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
({ attempt, task, repoId, targetBranch }) => {
|
||||
const modal = useModal();
|
||||
@@ -152,6 +157,7 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
config?.pr_auto_description_enabled ?? false
|
||||
);
|
||||
setCreatingPR(false);
|
||||
modal.resolve({ success: true } as CreatePRDialogResult);
|
||||
modal.hide();
|
||||
return;
|
||||
}
|
||||
@@ -231,6 +237,11 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
]);
|
||||
|
||||
const handleCancelCreatePR = useCallback(() => {
|
||||
// Return error if one was set, otherwise just canceled
|
||||
const result: CreatePRDialogResult = error
|
||||
? { success: false, error }
|
||||
: { success: false };
|
||||
modal.resolve(result);
|
||||
modal.hide();
|
||||
// Reset form to empty state
|
||||
setPrTitle('');
|
||||
@@ -238,7 +249,7 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
setPrBaseBranch('');
|
||||
setIsDraft(false);
|
||||
setAutoGenerateDescription(config?.pr_auto_description_enabled ?? false);
|
||||
}, [modal, config?.pr_auto_description_enabled]);
|
||||
}, [modal, config?.pr_auto_description_enabled, error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -380,6 +391,7 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
}
|
||||
);
|
||||
|
||||
export const CreatePRDialog = defineModal<CreatePRDialogProps, void>(
|
||||
CreatePRDialogImpl
|
||||
);
|
||||
export const CreatePRDialog = defineModal<
|
||||
CreatePRDialogProps,
|
||||
CreatePRDialogResult
|
||||
>(CreatePRDialogImpl);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import GitOperations from '@/components/tasks/Toolbar/GitOperations';
|
||||
import { useTaskAttempt } from '@/hooks/useTaskAttempt';
|
||||
import { useTaskAttemptWithSession } from '@/hooks/useTaskAttempt';
|
||||
import { useBranchStatus, useAttemptExecution } from '@/hooks';
|
||||
import { useAttemptRepo } from '@/hooks/useAttemptRepo';
|
||||
import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext';
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
GitOperationsProvider,
|
||||
useGitOperationsError,
|
||||
} from '@/contexts/GitOperationsContext';
|
||||
import type { Merge, TaskWithAttemptStatus, Workspace } from 'shared/types';
|
||||
import type { Merge, TaskWithAttemptStatus } from 'shared/types';
|
||||
import type { WorkspaceWithSession } from '@/types/attempt';
|
||||
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||
import { defineModal } from '@/lib/modals';
|
||||
|
||||
@@ -26,7 +27,7 @@ export interface GitActionsDialogProps {
|
||||
}
|
||||
|
||||
interface GitActionsDialogContentProps {
|
||||
attempt: Workspace;
|
||||
attempt: WorkspaceWithSession;
|
||||
task: TaskWithAttemptStatus;
|
||||
}
|
||||
|
||||
@@ -99,7 +100,7 @@ const GitActionsDialogImpl = NiceModal.create<GitActionsDialogProps>(
|
||||
const modal = useModal();
|
||||
const { t } = useTranslation('tasks');
|
||||
|
||||
const { data: attempt } = useTaskAttempt(attemptId);
|
||||
const { data: attempt } = useTaskAttemptWithSession(attemptId);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
@@ -125,6 +126,7 @@ const GitActionsDialogImpl = NiceModal.create<GitActionsDialogProps>(
|
||||
<ExecutionProcessesProvider
|
||||
key={attempt.id}
|
||||
attemptId={attempt.id}
|
||||
sessionId={attempt.session?.id}
|
||||
>
|
||||
<GitActionsDialogContent attempt={attempt} task={task} />
|
||||
</ExecutionProcessesProvider>
|
||||
|
||||
@@ -402,7 +402,6 @@ const TaskFormDialogImpl = NiceModal.create<TaskFormDialogProps>((props) => {
|
||||
<Dialog
|
||||
open={modal.visible}
|
||||
onOpenChange={handleDialogClose}
|
||||
className="w-full max-w-[min(90vw,40rem)] max-h-[min(95vh,50rem)] flex flex-col overflow-hidden"
|
||||
uncloseable={showDiscardWarning}
|
||||
>
|
||||
<div
|
||||
@@ -423,85 +422,80 @@ const TaskFormDialogImpl = NiceModal.create<TaskFormDialogProps>((props) => {
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<div className="flex-none px-4 py-2 border border-1 border-border">
|
||||
<form.Field name="title">
|
||||
{(field) => (
|
||||
<Input
|
||||
id="task-title"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder={t('taskFormDialog.titlePlaceholder')}
|
||||
className="text-lg font-semibold placeholder:text-muted-foreground/60 border-none p-0"
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
<form.Field name="title">
|
||||
{(field) => (
|
||||
<Input
|
||||
id="task-title"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder={t('taskFormDialog.titlePlaceholder')}
|
||||
disabled={isSubmitting}
|
||||
className="text-base"
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<div className="flex-1 p-4 min-h-0 overflow-y-auto overscroll-contain space-y-1 border border-1 border-border">
|
||||
{/* Description */}
|
||||
<form.Field name="description">
|
||||
{(field) => (
|
||||
{/* Description */}
|
||||
<form.Field name="description">
|
||||
{(field) => (
|
||||
<div className="border p-3">
|
||||
<WYSIWYGEditor
|
||||
placeholder={t('taskFormDialog.descriptionPlaceholder')}
|
||||
className="w-full h-24 overflow-auto"
|
||||
value={field.state.value}
|
||||
onChange={(desc) => field.handleChange(desc)}
|
||||
disabled={isSubmitting}
|
||||
projectId={projectId}
|
||||
onPasteFiles={onDrop}
|
||||
className="border-none shadow-none px-0 text-md font-normal"
|
||||
onCmdEnter={primaryAction}
|
||||
onShiftCmdEnter={handleSubmitCreateOnly}
|
||||
taskId={editMode ? props.task.id : undefined}
|
||||
localImages={localImages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
{/* Edit mode status */}
|
||||
{editMode && (
|
||||
<form.Field name="status">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-status" className="text-sm font-medium">
|
||||
{t('taskFormDialog.statusLabel')}
|
||||
</Label>
|
||||
<Select
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(value as TaskStatus)
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todo">
|
||||
{t('taskFormDialog.statusOptions.todo')}
|
||||
</SelectItem>
|
||||
<SelectItem value="inprogress">
|
||||
{t('taskFormDialog.statusOptions.inprogress')}
|
||||
</SelectItem>
|
||||
<SelectItem value="inreview">
|
||||
{t('taskFormDialog.statusOptions.inreview')}
|
||||
</SelectItem>
|
||||
<SelectItem value="done">
|
||||
{t('taskFormDialog.statusOptions.done')}
|
||||
</SelectItem>
|
||||
<SelectItem value="cancelled">
|
||||
{t('taskFormDialog.statusOptions.cancelled')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
{/* Edit mode status */}
|
||||
{editMode && (
|
||||
<form.Field name="status">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="task-status"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
{t('taskFormDialog.statusLabel')}
|
||||
</Label>
|
||||
<Select
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(value as TaskStatus)
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todo">
|
||||
{t('taskFormDialog.statusOptions.todo')}
|
||||
</SelectItem>
|
||||
<SelectItem value="inprogress">
|
||||
{t('taskFormDialog.statusOptions.inprogress')}
|
||||
</SelectItem>
|
||||
<SelectItem value="inreview">
|
||||
{t('taskFormDialog.statusOptions.inreview')}
|
||||
</SelectItem>
|
||||
<SelectItem value="done">
|
||||
{t('taskFormDialog.statusOptions.done')}
|
||||
</SelectItem>
|
||||
<SelectItem value="cancelled">
|
||||
{t('taskFormDialog.statusOptions.cancelled')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create mode dropdowns */}
|
||||
{!editMode && (
|
||||
@@ -511,7 +505,7 @@ const TaskFormDialogImpl = NiceModal.create<TaskFormDialogProps>((props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'py-2 my-2 transition-opacity duration-200',
|
||||
'transition-opacity duration-200',
|
||||
isSingleRepo ? '' : 'space-y-3',
|
||||
autoStartField.state.value
|
||||
? 'opacity-100'
|
||||
|
||||
@@ -233,6 +233,14 @@ export function Navbar() {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* <Button variant="ghost" size="sm" className="h-9 gap-1.5" asChild>
|
||||
<Link to="/workspaces">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{t('common:navbar.tryNewUI')}
|
||||
</Link>
|
||||
</Button>
|
||||
<NavDivider /> */}
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
9
frontend/src/components/layout/NewDesignLayout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function NewDesignLayout() {
|
||||
return (
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,12 @@ export function NormalLayout() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DevBanner />
|
||||
{!shouldHideNavbar && <Navbar />}
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<Outlet />
|
||||
<div className="flex flex-col h-screen">
|
||||
<DevBanner />
|
||||
{!shouldHideNavbar && <Navbar />}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import {
|
||||
Group,
|
||||
Panel,
|
||||
Separator,
|
||||
useDefaultLayout,
|
||||
type PanelSize,
|
||||
} from 'react-resizable-panels';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -15,37 +21,8 @@ interface TasksLayoutProps {
|
||||
rightHeader?: ReactNode;
|
||||
}
|
||||
|
||||
type SplitSizes = [number, number];
|
||||
|
||||
const MIN_PANEL_SIZE = 20;
|
||||
const DEFAULT_KANBAN_ATTEMPT: SplitSizes = [66, 34];
|
||||
const DEFAULT_ATTEMPT_AUX: SplitSizes = [34, 66];
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
KANBAN_ATTEMPT: 'tasksLayout.desktop.v2.kanbanAttempt',
|
||||
ATTEMPT_AUX: 'tasksLayout.desktop.v2.attemptAux',
|
||||
} as const;
|
||||
|
||||
function loadSizes(key: string, fallback: SplitSizes): SplitSizes {
|
||||
try {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (!saved) return fallback;
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed) && parsed.length === 2)
|
||||
return parsed as SplitSizes;
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSizes(key: string, sizes: SplitSizes): void {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(sizes));
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
const MIN_PANEL_SIZE = 20; // percentage (0-100)
|
||||
const COLLAPSED_SIZE = 0; // percentage (0-100)
|
||||
|
||||
/**
|
||||
* AuxRouter - Handles nested AnimatePresence for preview/diffs transitions.
|
||||
@@ -84,11 +61,16 @@ function RightWorkArea({
|
||||
mode: LayoutMode;
|
||||
rightHeader?: ReactNode;
|
||||
}) {
|
||||
const [innerSizes] = useState<SplitSizes>(() =>
|
||||
loadSizes(STORAGE_KEYS.ATTEMPT_AUX, DEFAULT_ATTEMPT_AUX)
|
||||
);
|
||||
const { defaultLayout, onLayoutChange } = useDefaultLayout({
|
||||
groupId: 'tasksLayout-attemptAux',
|
||||
storage: localStorage,
|
||||
});
|
||||
const [isAttemptCollapsed, setIsAttemptCollapsed] = useState(false);
|
||||
|
||||
const handleAttemptResize = (size: PanelSize) => {
|
||||
setIsAttemptCollapsed(size.asPercentage === COLLAPSED_SIZE);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 flex flex-col">
|
||||
{rightHeader && (
|
||||
@@ -100,24 +82,19 @@ function RightWorkArea({
|
||||
{mode === null ? (
|
||||
attempt
|
||||
) : (
|
||||
<PanelGroup
|
||||
direction="horizontal"
|
||||
<Group
|
||||
orientation="horizontal"
|
||||
className="h-full min-h-0"
|
||||
onLayout={(layout) => {
|
||||
if (layout.length === 2) {
|
||||
saveSizes(STORAGE_KEYS.ATTEMPT_AUX, [layout[0], layout[1]]);
|
||||
}
|
||||
}}
|
||||
defaultLayout={defaultLayout}
|
||||
onLayoutChange={onLayoutChange}
|
||||
>
|
||||
<Panel
|
||||
id="attempt"
|
||||
order={1}
|
||||
defaultSize={innerSizes[0]}
|
||||
defaultSize={34}
|
||||
minSize={MIN_PANEL_SIZE}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
onCollapse={() => setIsAttemptCollapsed(true)}
|
||||
onExpand={() => setIsAttemptCollapsed(false)}
|
||||
collapsedSize={COLLAPSED_SIZE}
|
||||
onResize={handleAttemptResize}
|
||||
className="min-w-0 min-h-0 overflow-hidden"
|
||||
role="region"
|
||||
aria-label="Details"
|
||||
@@ -125,7 +102,7 @@ function RightWorkArea({
|
||||
{attempt}
|
||||
</Panel>
|
||||
|
||||
<PanelResizeHandle
|
||||
<Separator
|
||||
id="handle-aa"
|
||||
className={cn(
|
||||
'relative z-30 bg-border cursor-col-resize group touch-none',
|
||||
@@ -135,8 +112,6 @@ function RightWorkArea({
|
||||
isAttemptCollapsed ? 'w-6' : 'w-1'
|
||||
)}
|
||||
aria-label="Resize panels"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-1/2 -translate-x-1/2 w-px bg-border" />
|
||||
<div className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-1 bg-muted/90 border border-border rounded-full px-1.5 py-3 opacity-70 group-hover:opacity-100 group-focus:opacity-100 transition-opacity shadow-sm">
|
||||
@@ -144,21 +119,19 @@ function RightWorkArea({
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground" />
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground" />
|
||||
</div>
|
||||
</PanelResizeHandle>
|
||||
</Separator>
|
||||
|
||||
<Panel
|
||||
id="aux"
|
||||
order={2}
|
||||
defaultSize={innerSizes[1]}
|
||||
defaultSize={66}
|
||||
minSize={MIN_PANEL_SIZE}
|
||||
collapsible={false}
|
||||
className="min-w-0 min-h-0 overflow-hidden"
|
||||
role="region"
|
||||
aria-label={mode === 'preview' ? 'Preview' : 'Diffs'}
|
||||
>
|
||||
<AuxRouter mode={mode} aux={aux} />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,11 +156,16 @@ function DesktopSimple({
|
||||
mode: LayoutMode;
|
||||
rightHeader?: ReactNode;
|
||||
}) {
|
||||
const [outerSizes] = useState<SplitSizes>(() =>
|
||||
loadSizes(STORAGE_KEYS.KANBAN_ATTEMPT, DEFAULT_KANBAN_ATTEMPT)
|
||||
);
|
||||
const { defaultLayout, onLayoutChange } = useDefaultLayout({
|
||||
groupId: 'tasksLayout-kanbanAttempt',
|
||||
storage: localStorage,
|
||||
});
|
||||
const [isKanbanCollapsed, setIsKanbanCollapsed] = useState(false);
|
||||
|
||||
const handleKanbanResize = (size: PanelSize) => {
|
||||
setIsKanbanCollapsed(size.asPercentage === COLLAPSED_SIZE);
|
||||
};
|
||||
|
||||
// When preview/diffs is open, hide Kanban entirely and render only RightWorkArea
|
||||
if (mode !== null) {
|
||||
return (
|
||||
@@ -202,24 +180,19 @@ function DesktopSimple({
|
||||
|
||||
// When only viewing attempt logs, show Kanban | Attempt (no aux)
|
||||
return (
|
||||
<PanelGroup
|
||||
direction="horizontal"
|
||||
<Group
|
||||
orientation="horizontal"
|
||||
className="h-full min-h-0"
|
||||
onLayout={(layout) => {
|
||||
if (layout.length === 2) {
|
||||
saveSizes(STORAGE_KEYS.KANBAN_ATTEMPT, [layout[0], layout[1]]);
|
||||
}
|
||||
}}
|
||||
defaultLayout={defaultLayout}
|
||||
onLayoutChange={onLayoutChange}
|
||||
>
|
||||
<Panel
|
||||
id="kanban"
|
||||
order={1}
|
||||
defaultSize={outerSizes[0]}
|
||||
defaultSize={66}
|
||||
minSize={MIN_PANEL_SIZE}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
onCollapse={() => setIsKanbanCollapsed(true)}
|
||||
onExpand={() => setIsKanbanCollapsed(false)}
|
||||
collapsedSize={COLLAPSED_SIZE}
|
||||
onResize={handleKanbanResize}
|
||||
className="min-w-0 min-h-0 overflow-hidden"
|
||||
role="region"
|
||||
aria-label="Kanban board"
|
||||
@@ -227,7 +200,7 @@ function DesktopSimple({
|
||||
{kanban}
|
||||
</Panel>
|
||||
|
||||
<PanelResizeHandle
|
||||
<Separator
|
||||
id="handle-kr"
|
||||
className={cn(
|
||||
'relative z-30 bg-border cursor-col-resize group touch-none',
|
||||
@@ -237,8 +210,6 @@ function DesktopSimple({
|
||||
isKanbanCollapsed ? 'w-6' : 'w-1'
|
||||
)}
|
||||
aria-label="Resize panels"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-1/2 -translate-x-1/2 w-px bg-border" />
|
||||
<div className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-1 bg-muted/90 border border-border rounded-full px-1.5 py-3 opacity-70 group-hover:opacity-100 group-focus:opacity-100 transition-opacity shadow-sm">
|
||||
@@ -246,14 +217,12 @@ function DesktopSimple({
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground" />
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground" />
|
||||
</div>
|
||||
</PanelResizeHandle>
|
||||
</Separator>
|
||||
|
||||
<Panel
|
||||
id="right"
|
||||
order={2}
|
||||
defaultSize={outerSizes[1]}
|
||||
defaultSize={34}
|
||||
minSize={MIN_PANEL_SIZE}
|
||||
collapsible={false}
|
||||
className="min-w-0 min-h-0 overflow-hidden"
|
||||
>
|
||||
<RightWorkArea
|
||||
@@ -263,7 +232,7 @@ function DesktopSimple({
|
||||
rightHeader={rightHeader}
|
||||
/>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
19
frontend/src/components/legacy-design/LegacyDesignScope.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import { PortalContainerContext } from '@/contexts/PortalContainerContext';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import '@/styles/legacy/index.css';
|
||||
|
||||
interface LegacyDesignScopeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LegacyDesignScope({ children }: LegacyDesignScopeProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<div ref={ref} className="legacy-design min-h-screen">
|
||||
<PortalContainerContext.Provider value={ref}>
|
||||
<NiceModal.Provider>{children}</NiceModal.Provider>
|
||||
</PortalContainerContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ const VirtualizedList = ({ attempt, task }: VirtualizedListProps) => {
|
||||
) => {
|
||||
let scrollModifier: ScrollModifier = InitialDataScrollModifier;
|
||||
|
||||
if (addType === 'running' && !loading) {
|
||||
if ((addType === 'running' || addType === 'plan') && !loading) {
|
||||
scrollModifier = AutoScrollToBottom;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,25 @@ export const AttemptHeaderActions = ({
|
||||
{t('attemptHeaderActions.diffs')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{/* {attempt?.id && (
|
||||
<>
|
||||
<div className="h-4 w-px bg-border" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
to={`/workspaces/${attempt.id}`}
|
||||
className="inline-flex items-center justify-center text-primary-foreground/70 hover:text-accent-foreground"
|
||||
aria-label="Try the new UI"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t('attemptHeaderActions.tryNewUI')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)} */}
|
||||
</ToggleGroup>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ProjectList() {
|
||||
const handleCreateProject = async () => {
|
||||
try {
|
||||
const result = await ProjectFormDialog.show({});
|
||||
if (result === 'saved') return;
|
||||
if (result.status === 'saved') return;
|
||||
} catch (error) {
|
||||
// User cancelled - do nothing
|
||||
}
|
||||
|
||||