feat(agents): role-specific tabs + bttask Tauri backend

- TaskBoardTab: kanban board (5 columns, CRUD, comments, 5s poll) for Manager
- ArchitectureTab: PlantUML viewer/editor (4 templates, plantuml.com) for Architect
- TestingTab: Selenium screenshots + test file discovery for Tester
- bttask.rs: Rust backend (list, create, update_status, delete, comments)
- bttask-bridge.ts: TypeScript IPC adapter
- ProjectBox: conditional role tabs (isAgent && agentRole), PERSISTED-LAZY
This commit is contained in:
DexterFromLab 2026-03-11 15:25:41 +01:00
parent 0c28f204c7
commit 2ca7756a74
9 changed files with 1812 additions and 2 deletions

169
v2/src-tauri/src/bttask.rs Normal file
View file

@ -0,0 +1,169 @@
// bttask — Read access to task board SQLite tables in btmsg.db
// Tasks table created by bttask CLI, shared DB with btmsg
use rusqlite::{params, Connection, OpenFlags};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
fn db_path() -> PathBuf {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("bterminal")
.join("btmsg.db")
}
fn open_db() -> Result<Connection, String> {
let path = db_path();
if !path.exists() {
return Err("btmsg database not found".into());
}
Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
.map_err(|e| format!("Failed to open btmsg.db: {e}"))
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
pub id: String,
pub title: String,
pub description: String,
pub status: String,
pub priority: String,
pub assigned_to: Option<String>,
pub created_by: String,
pub group_id: String,
pub parent_task_id: Option<String>,
pub sort_order: i32,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskComment {
pub id: String,
pub task_id: String,
pub agent_id: String,
pub content: String,
pub created_at: String,
}
/// Get all tasks for a group
pub fn list_tasks(group_id: &str) -> Result<Vec<Task>, String> {
let db = open_db()?;
let mut stmt = db
.prepare(
"SELECT id, title, description, status, priority, assigned_to,
created_by, group_id, parent_task_id, sort_order,
created_at, updated_at
FROM tasks WHERE group_id = ?1
ORDER BY sort_order ASC, created_at DESC",
)
.map_err(|e| format!("Query error: {e}"))?;
let rows = stmt
.query_map(params![group_id], |row| {
Ok(Task {
id: row.get(0)?,
title: row.get(1)?,
description: row.get::<_, String>(2).unwrap_or_default(),
status: row.get::<_, String>(3).unwrap_or_else(|_| "todo".into()),
priority: row.get::<_, String>(4).unwrap_or_else(|_| "medium".into()),
assigned_to: row.get(5)?,
created_by: row.get(6)?,
group_id: row.get(7)?,
parent_task_id: row.get(8)?,
sort_order: row.get::<_, i32>(9).unwrap_or(0),
created_at: row.get::<_, String>(10).unwrap_or_default(),
updated_at: row.get::<_, String>(11).unwrap_or_default(),
})
})
.map_err(|e| format!("Query error: {e}"))?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row error: {e}"))
}
/// Get comments for a task
pub fn task_comments(task_id: &str) -> Result<Vec<TaskComment>, String> {
let db = open_db()?;
let mut stmt = db
.prepare(
"SELECT id, task_id, agent_id, content, created_at
FROM task_comments WHERE task_id = ?1
ORDER BY created_at ASC",
)
.map_err(|e| format!("Query error: {e}"))?;
let rows = stmt
.query_map(params![task_id], |row| {
Ok(TaskComment {
id: row.get(0)?,
task_id: row.get(1)?,
agent_id: row.get(2)?,
content: row.get(3)?,
created_at: row.get::<_, String>(4).unwrap_or_default(),
})
})
.map_err(|e| format!("Query error: {e}"))?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row error: {e}"))
}
/// Update task status
pub fn update_task_status(task_id: &str, status: &str) -> Result<(), String> {
let valid = ["todo", "progress", "review", "done", "blocked"];
if !valid.contains(&status) {
return Err(format!("Invalid status '{}'. Valid: {:?}", status, valid));
}
let db = open_db()?;
db.execute(
"UPDATE tasks SET status = ?1, updated_at = datetime('now') WHERE id = ?2",
params![status, task_id],
)
.map_err(|e| format!("Update error: {e}"))?;
Ok(())
}
/// Add a comment to a task
pub fn add_comment(task_id: &str, agent_id: &str, content: &str) -> Result<String, String> {
let db = open_db()?;
let id = uuid::Uuid::new_v4().to_string();
db.execute(
"INSERT INTO task_comments (id, task_id, agent_id, content) VALUES (?1, ?2, ?3, ?4)",
params![id, task_id, agent_id, content],
)
.map_err(|e| format!("Insert error: {e}"))?;
Ok(id)
}
/// Create a new task
pub fn create_task(
title: &str,
description: &str,
priority: &str,
group_id: &str,
created_by: &str,
assigned_to: Option<&str>,
) -> Result<String, String> {
let db = open_db()?;
let id = uuid::Uuid::new_v4().to_string();
db.execute(
"INSERT INTO tasks (id, title, description, priority, group_id, created_by, assigned_to)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![id, title, description, priority, group_id, created_by, assigned_to],
)
.map_err(|e| format!("Insert error: {e}"))?;
Ok(id)
}
/// Delete a task
pub fn delete_task(task_id: &str) -> Result<(), String> {
let db = open_db()?;
db.execute("DELETE FROM task_comments WHERE task_id = ?1", params![task_id])
.map_err(|e| format!("Delete comments error: {e}"))?;
db.execute("DELETE FROM tasks WHERE id = ?1", params![task_id])
.map_err(|e| format!("Delete task error: {e}"))?;
Ok(())
}

View file

@ -0,0 +1,38 @@
use crate::bttask;
#[tauri::command]
pub fn bttask_list(group_id: String) -> Result<Vec<bttask::Task>, String> {
bttask::list_tasks(&group_id)
}
#[tauri::command]
pub fn bttask_comments(task_id: String) -> Result<Vec<bttask::TaskComment>, String> {
bttask::task_comments(&task_id)
}
#[tauri::command]
pub fn bttask_update_status(task_id: String, status: String) -> Result<(), String> {
bttask::update_task_status(&task_id, &status)
}
#[tauri::command]
pub fn bttask_add_comment(task_id: String, agent_id: String, content: String) -> Result<String, String> {
bttask::add_comment(&task_id, &agent_id, &content)
}
#[tauri::command]
pub fn bttask_create(
title: String,
description: String,
priority: String,
group_id: String,
created_by: String,
assigned_to: Option<String>,
) -> Result<String, String> {
bttask::create_task(&title, &description, &priority, &group_id, &created_by, assigned_to.as_deref())
}
#[tauri::command]
pub fn bttask_delete(task_id: String) -> Result<(), String> {
bttask::delete_task(&task_id)
}

View file

@ -10,3 +10,4 @@ pub mod files;
pub mod remote;
pub mod misc;
pub mod btmsg;
pub mod bttask;

View file

@ -1,4 +1,5 @@
mod btmsg;
mod bttask;
mod commands;
mod ctx;
mod event_sink;
@ -139,6 +140,13 @@ pub fn run() {
commands::btmsg::btmsg_channel_send,
commands::btmsg::btmsg_create_channel,
commands::btmsg::btmsg_add_channel_member,
// bttask (task board)
commands::bttask::bttask_list,
commands::bttask::bttask_comments,
commands::bttask::bttask_update_status,
commands::bttask::bttask_add_comment,
commands::bttask::bttask_create,
commands::bttask::bttask_delete,
// Misc
commands::misc::cli_get_group,
commands::misc::open_url,