diff --git a/v2/src-tauri/src/bttask.rs b/v2/src-tauri/src/bttask.rs new file mode 100644 index 0000000..af4f833 --- /dev/null +++ b/v2/src-tauri/src/bttask.rs @@ -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 { + 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, + pub created_by: String, + pub group_id: String, + pub parent_task_id: Option, + 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, 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::, _>>() + .map_err(|e| format!("Row error: {e}")) +} + +/// Get comments for a task +pub fn task_comments(task_id: &str) -> Result, 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::, _>>() + .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 { + 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 { + 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(()) +} diff --git a/v2/src-tauri/src/commands/bttask.rs b/v2/src-tauri/src/commands/bttask.rs new file mode 100644 index 0000000..395c69a --- /dev/null +++ b/v2/src-tauri/src/commands/bttask.rs @@ -0,0 +1,38 @@ +use crate::bttask; + +#[tauri::command] +pub fn bttask_list(group_id: String) -> Result, String> { + bttask::list_tasks(&group_id) +} + +#[tauri::command] +pub fn bttask_comments(task_id: String) -> Result, 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 { + 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, +) -> Result { + 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) +} diff --git a/v2/src-tauri/src/commands/mod.rs b/v2/src-tauri/src/commands/mod.rs index f18d757..76c49a6 100644 --- a/v2/src-tauri/src/commands/mod.rs +++ b/v2/src-tauri/src/commands/mod.rs @@ -10,3 +10,4 @@ pub mod files; pub mod remote; pub mod misc; pub mod btmsg; +pub mod bttask; diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index 6761f1f..91bd9ad 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -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, diff --git a/v2/src/lib/adapters/bttask-bridge.ts b/v2/src/lib/adapters/bttask-bridge.ts new file mode 100644 index 0000000..7146b8f --- /dev/null +++ b/v2/src/lib/adapters/bttask-bridge.ts @@ -0,0 +1,57 @@ +// bttask Bridge โ€” Tauri IPC adapter for task board + +import { invoke } from '@tauri-apps/api/core'; + +export interface Task { + id: string; + title: string; + description: string; + status: 'todo' | 'progress' | 'review' | 'done' | 'blocked'; + priority: 'low' | 'medium' | 'high' | 'critical'; + assignedTo: string | null; + createdBy: string; + groupId: string; + parentTaskId: string | null; + sortOrder: number; + createdAt: string; + updatedAt: string; +} + +export interface TaskComment { + id: string; + taskId: string; + agentId: string; + content: string; + createdAt: string; +} + +export async function listTasks(groupId: string): Promise { + return invoke('bttask_list', { groupId }); +} + +export async function getTaskComments(taskId: string): Promise { + return invoke('bttask_comments', { taskId }); +} + +export async function updateTaskStatus(taskId: string, status: string): Promise { + return invoke('bttask_update_status', { taskId, status }); +} + +export async function addTaskComment(taskId: string, agentId: string, content: string): Promise { + return invoke('bttask_add_comment', { taskId, agentId, content }); +} + +export async function createTask( + title: string, + description: string, + priority: string, + groupId: string, + createdBy: string, + assignedTo?: string, +): Promise { + return invoke('bttask_create', { title, description, priority, groupId, createdBy, assignedTo }); +} + +export async function deleteTask(taskId: string): Promise { + return invoke('bttask_delete', { taskId }); +} diff --git a/v2/src/lib/components/Workspace/ArchitectureTab.svelte b/v2/src/lib/components/Workspace/ArchitectureTab.svelte new file mode 100644 index 0000000..1e04907 --- /dev/null +++ b/v2/src/lib/components/Workspace/ArchitectureTab.svelte @@ -0,0 +1,493 @@ + + +
+
+ + + {#if showNewForm} +
+ +
+ {#each Object.entries(DIAGRAM_TEMPLATES) as [name, template]} + + {/each} +
+
+ {/if} + +
+ {#each diagrams as file (file.path)} + + {/each} + {#if diagrams.length === 0 && !showNewForm} +
+ No diagrams yet. The Architect agent creates .puml files in {ARCH_DIR}/ +
+ {/if} +
+
+ +
+ {#if !selectedFile} +
+ Select a diagram or create a new one +
+ {:else if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else} +
+ {selectedFile?.split('/').pop()} + + {#if editing} + + {/if} +
+ + {#if editing} + + {:else if svgUrl} +
+ PlantUML diagram +
+ {/if} + {/if} +
+
+ + diff --git a/v2/src/lib/components/Workspace/ProjectBox.svelte b/v2/src/lib/components/Workspace/ProjectBox.svelte index cf4565d..c3a0813 100644 --- a/v2/src/lib/components/Workspace/ProjectBox.svelte +++ b/v2/src/lib/components/Workspace/ProjectBox.svelte @@ -11,7 +11,10 @@ import FilesTab from './FilesTab.svelte'; import SshTab from './SshTab.svelte'; import MemoriesTab from './MemoriesTab.svelte'; - import { getTerminalTabs } from '../../stores/workspace.svelte'; + import TaskBoardTab from './TaskBoardTab.svelte'; + import ArchitectureTab from './ArchitectureTab.svelte'; + import TestingTab from './TestingTab.svelte'; + import { getTerminalTabs, getActiveGroup } from '../../stores/workspace.svelte'; import { getProjectHealth, setStallThreshold } from '../../stores/health.svelte'; import { fsWatchProject, fsUnwatchProject, onFsWriteDetected, fsWatcherStatus } from '../../adapters/fs-watcher-bridge'; import { recordExternalWrite } from '../../stores/conflicts.svelte'; @@ -31,9 +34,13 @@ let mainSessionId = $state(null); let terminalExpanded = $state(false); - type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memories'; + type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memories' | 'tasks' | 'architecture' | 'selenium' | 'tests'; let activeTab = $state('model'); + let activeGroup = $derived(getActiveGroup()); + let agentRole = $derived(project.agentRole); + let isAgent = $derived(project.isAgent ?? false); + // PERSISTED-LAZY: track which tabs have been activated at least once let everActivated = $state>({}); @@ -149,6 +156,16 @@ class:active={activeTab === 'memories'} onclick={() => switchTab('memories')} >Memory + {#if isAgent && agentRole === 'manager'} + + {/if} + {#if isAgent && agentRole === 'architect'} + + {/if} + {#if isAgent && agentRole === 'tester'} + + + {/if}
@@ -182,6 +199,26 @@
{/if} + {#if everActivated['tasks'] && activeGroup} +
+ +
+ {/if} + {#if everActivated['architecture']} +
+ +
+ {/if} + {#if everActivated['selenium']} +
+ +
+ {/if} + {#if everActivated['tests']} +
+ +
+ {/if}
@@ -267,6 +304,14 @@ margin-bottom: -1px; } + .ptab-role { + color: var(--ctp-mauve); + } + + .ptab-role:hover { + color: var(--ctp-text); + } + .project-content-area { overflow: hidden; position: relative; diff --git a/v2/src/lib/components/Workspace/TaskBoardTab.svelte b/v2/src/lib/components/Workspace/TaskBoardTab.svelte new file mode 100644 index 0000000..9ca1299 --- /dev/null +++ b/v2/src/lib/components/Workspace/TaskBoardTab.svelte @@ -0,0 +1,572 @@ + + +
+
+ Task Board + + {pendingCount === 0 ? 'All done' : `${pendingCount} pending`} + + +
+ + {#if showAddForm} +
+ { if (e.key === 'Enter') handleAddTask(); }} + /> + +
+ + +
+
+ {/if} + + {#if loading} +
Loading tasks...
+ {:else if error} +
{error}
+ {:else} +
+ {#each STATUSES as status} +
+
+ {STATUS_ICONS[status]} + {STATUS_LABELS[status]} + {tasksByStatus[status].length} +
+
+ {#each tasksByStatus[status] as task (task.id)} +
+ + + {#if expandedTaskId === task.id} +
+ {#if task.description} +

{task.description}

+ {/if} + +
+ {#each STATUSES as s} + + {/each} +
+ + {#if taskComments.length > 0} +
+ {#each taskComments as comment} +
+ {comment.agentId} + {comment.content} +
+ {/each} +
+ {/if} + +
+ { if (e.key === 'Enter') handleAddComment(); }} + /> +
+ + +
+ {/if} +
+ {/each} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/v2/src/lib/components/Workspace/TestingTab.svelte b/v2/src/lib/components/Workspace/TestingTab.svelte new file mode 100644 index 0000000..52ea06b --- /dev/null +++ b/v2/src/lib/components/Workspace/TestingTab.svelte @@ -0,0 +1,427 @@ + + +
+ {#if mode === 'selenium'} + +
+
+ +
+ {#each screenshots as path} + + {/each} + {#if screenshots.length === 0} +
+ No screenshots yet. The Tester agent saves screenshots to {SCREENSHOTS_DIR}/ +
+ {/if} +
+ +
+ +
+ {#each seleniumLog as line} +
{line}
+ {/each} + {#if seleniumLog.length === 0} +
No log entries
+ {/if} +
+
+
+ +
+ {#if selectedScreenshot} +
+ Selenium screenshot +
+ {:else} +
+ Selenium screenshots will appear here during testing. +
+ The Tester agent uses Selenium WebDriver for UI testing. +
+ {/if} +
+
+ + {:else} + +
+
+ +
+ {#each testFiles as file (file.path)} + + {/each} + {#if testFiles.length === 0} +
+ No test files found. The Tester agent creates tests in standard directories (tests/, test/, spec/). +
+ {/if} +
+
+ +
+ {#if selectedTestFile} +
+ {selectedTestFile.split('/').pop()} +
+
{testOutput}
+ {:else} +
+ Select a test file to view its contents. +
+ The Tester agent runs tests via the terminal. +
+ {/if} +
+
+ {/if} +
+ +