# Orchai Phase 1: Foundation Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** Get a working Tauri 2 desktop app with SQLite storage and full Project CRUD (create from local path or clone URL, list, edit, delete) with a React UI. **Architecture:** Tauri 2 (Rust backend) with React + TypeScript frontend. SQLite database via rusqlite for persistence. Tauri IPC commands expose backend operations to the frontend. React Router for navigation with a sidebar-based layout. **Tech Stack:** Rust, Tauri 2, React 18, TypeScript, Vite, Tailwind CSS, SQLite (rusqlite), react-router-dom v6 --- ## Phasing Strategy This is Plan 1 of 4: - **Plan 1 (this):** Foundation -- Tauri scaffold, SQLite, Project Manager - **Plan 2:** Tuleap Integration -- credentials, API client, poller, filter engine, tracker config - **Plan 3:** Agent Pipeline -- orchestrator, worktree manager, ticket processing, results UI - **Plan 4:** Notifications + Polish -- notifier, system notifications, dashboard --- ## File Structure ``` orchai/ ├── docs/ # existing ├── src-tauri/ │ ├── Cargo.toml # modify: add rusqlite, uuid, chrono, serde │ ├── build.rs # from scaffold │ ├── tauri.conf.json # modify: app name, identifier, permissions │ ├── capabilities/ │ │ └── default.json # modify: add dialog permissions │ ├── migrations/ │ │ └── 001_init.sql # create: full schema (all tables) │ └── src/ │ ├── main.rs # from scaffold (unchanged) │ ├── lib.rs # modify: setup app state, register commands │ ├── db.rs # create: SQLite init + migration runner │ ├── error.rs # create: shared error type │ ├── models/ │ │ ├── mod.rs # create: re-exports │ │ └── project.rs # create: Project struct + CRUD │ └── commands/ │ ├── mod.rs # create: re-exports │ └── project.rs # create: Tauri commands for project CRUD ├── src/ │ ├── main.tsx # from scaffold (unchanged) │ ├── App.tsx # modify: router setup │ ├── App.css # delete (replaced by Tailwind) │ ├── index.css # modify: Tailwind directives │ ├── lib/ │ │ ├── types.ts # create: TypeScript types matching Rust models │ │ └── api.ts # create: typed invoke wrappers │ ├── components/ │ │ ├── layout/ │ │ │ ├── AppLayout.tsx # create: sidebar + main content area │ │ │ └── Sidebar.tsx # create: project list + add button │ │ └── projects/ │ │ ├── ProjectList.tsx # create: empty state / project cards │ │ ├── ProjectForm.tsx # create: create/edit form with folder picker │ │ └── ProjectDashboard.tsx # create: project overview (placeholder) ├── index.html # from scaffold ├── package.json # modify: add dependencies ├── vite.config.ts # from scaffold ├── tsconfig.json # from scaffold ├── tsconfig.node.json # from scaffold ├── tailwind.config.js # create ├── postcss.config.js # create └── .gitignore # from scaffold ``` --- ### Task 1: Scaffold Tauri 2 + React + TypeScript **Files:** - Create: entire project scaffold via CLI - Preserve: `docs/`, `.git/` - [x] **Step 1: Save existing repo contents** ```bash cd /home/leclere/Projets cp -r orchai/docs /tmp/orchai-docs-backup cp -r orchai/.git /tmp/orchai-git-backup ``` - [x] **Step 2: Scaffold Tauri 2 project** ```bash cd /home/leclere/Projets rm -rf orchai npm create tauri-app@latest orchai ``` When prompted, select: - Project name: `orchai` - Identifier: `com.orchai.app` - Frontend language: `TypeScript / JavaScript` - Package manager: `npm` - UI template: `React` - UI flavor: `TypeScript` - [x] **Step 3: Restore repo history and docs** ```bash cd /home/leclere/Projets/orchai rm -rf .git cp -r /tmp/orchai-git-backup .git cp -r /tmp/orchai-docs-backup docs rm -rf /tmp/orchai-docs-backup /tmp/orchai-git-backup ``` - [x] **Step 4: Install dependencies and verify** ```bash cd /home/leclere/Projets/orchai npm install cd src-tauri && cargo build cd .. ``` Expected: build succeeds with no errors. - [x] **Step 5: Verify dev server starts** ```bash npm run tauri dev ``` Expected: Tauri window opens with the default React starter page. Close it after verifying. - [x] **Step 6: Commit** ```bash git add -A git commit -m "scaffold: Tauri 2 + React + TypeScript via create-tauri-app" ``` --- ### Task 2: Configure Tailwind CSS + project metadata **Files:** - Create: `tailwind.config.js`, `postcss.config.js` - Modify: `src/index.css`, `package.json`, `src-tauri/tauri.conf.json` - Delete: `src/App.css` - [x] **Step 1: Install Tailwind** ```bash cd /home/leclere/Projets/orchai npm install -D tailwindcss @tailwindcss/vite ``` - [x] **Step 2: Add Tailwind to Vite config** Replace the contents of `vite.config.ts`: ```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; const host = process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [react(), tailwindcss()], clearScreen: false, server: { port: 1420, strictPort: true, host: host || false, hmr: host ? { protocol: "ws", host, port: 1421, } : undefined, watch: { ignored: ["**/src-tauri/**"], }, }, })); ``` - [x] **Step 3: Replace index.css with Tailwind directives** Replace the contents of `src/index.css`: ```css @import "tailwindcss"; ``` - [x] **Step 4: Delete App.css and clean up App.tsx** Delete `src/App.css`. Replace `src/App.tsx`: ```tsx function App() { return (

Orchai

); } export default App; ``` - [x] **Step 5: Update Tauri config** In `src-tauri/tauri.conf.json`, update the `app` section: ```json { "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json", "productName": "Orchai", "version": "0.1.0", "identifier": "com.orchai.app", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1420", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build" }, "app": { "title": "Orchai", "windows": [ { "title": "Orchai", "width": 1200, "height": 800 } ], "security": { "csp": null } } } ``` - [x] **Step 6: Verify Tailwind works** ```bash npm run tauri dev ``` Expected: window opens, "Orchai" heading rendered with Tailwind styling (bold, large). Close after verifying. - [x] **Step 7: Commit** ```bash git add -A git commit -m "configure: Tailwind CSS + app metadata" ``` --- ### Task 3: SQLite database + migration system **Files:** - Modify: `src-tauri/Cargo.toml` - Create: `src-tauri/migrations/001_init.sql` - Create: `src-tauri/src/db.rs` - Create: `src-tauri/src/error.rs` - Modify: `src-tauri/src/lib.rs` - [x] **Step 1: Write the failing test for db initialization** Add dependencies to `src-tauri/Cargo.toml` under `[dependencies]`: ```toml rusqlite = { version = "0.31", features = ["bundled"] } uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } ``` Create `src-tauri/src/db.rs`: ```rust use rusqlite::{Connection, Result}; use std::path::Path; pub fn init(db_path: &Path) -> Result { todo!() } pub fn init_in_memory() -> Result { todo!() } #[cfg(test)] mod tests { use super::*; #[test] fn test_init_in_memory_creates_tables() { let conn = init_in_memory().expect("should initialize"); // Verify all 6 tables exist let tables: Vec = conn .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name") .unwrap() .query_map([], |row| row.get(0)) .unwrap() .collect::, _>>() .unwrap(); assert_eq!( tables, vec![ "notifications", "processed_tickets", "projects", "tuleap_credentials", "watched_trackers", "worktrees", ] ); } #[test] fn test_init_in_memory_enables_foreign_keys() { let conn = init_in_memory().expect("should initialize"); let fk_enabled: i32 = conn .query_row("PRAGMA foreign_keys", [], |row| row.get(0)) .unwrap(); assert_eq!(fk_enabled, 1); } #[test] fn test_init_in_memory_sets_wal_mode() { let conn = init_in_memory().expect("should initialize"); let mode: String = conn .query_row("PRAGMA journal_mode", [], |row| row.get(0)) .unwrap(); assert_eq!(mode, "wal"); } } ``` - [x] **Step 2: Run tests to verify they fail** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo test db::tests ``` Expected: 3 failures with `not yet implemented`. - [x] **Step 3: Create migration SQL** Create `src-tauri/migrations/001_init.sql`: ```sql CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT NOT NULL, cloned_from TEXT, base_branch TEXT NOT NULL DEFAULT 'main', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS tuleap_credentials ( id TEXT PRIMARY KEY, tuleap_url TEXT NOT NULL, username TEXT NOT NULL, password_encrypted TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS watched_trackers ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, tracker_id INTEGER NOT NULL, tracker_label TEXT NOT NULL, polling_interval INTEGER NOT NULL DEFAULT 10, agent_config_json TEXT NOT NULL, filters_json TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS processed_tickets ( id TEXT PRIMARY KEY, tracker_id TEXT NOT NULL REFERENCES watched_trackers(id) ON DELETE CASCADE, artifact_id INTEGER NOT NULL, artifact_title TEXT NOT NULL, artifact_data TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'Pending', analyst_report TEXT, developer_report TEXT, detected_at TEXT NOT NULL DEFAULT (datetime('now')), processed_at TEXT ); CREATE TABLE IF NOT EXISTS worktrees ( id TEXT PRIMARY KEY, ticket_id TEXT NOT NULL REFERENCES processed_tickets(id), path TEXT NOT NULL, branch_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'Active', created_at TEXT NOT NULL DEFAULT (datetime('now')), merged_at TEXT, merged_into TEXT ); CREATE TABLE IF NOT EXISTS notifications ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, ticket_id TEXT REFERENCES processed_tickets(id), type TEXT NOT NULL, title TEXT NOT NULL, message TEXT NOT NULL, read INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` - [x] **Step 4: Implement db::init and db::init_in_memory** Replace the `todo!()` implementations in `src-tauri/src/db.rs`: ```rust use rusqlite::{Connection, Result}; use std::path::Path; const MIGRATION_001: &str = include_str!("../migrations/001_init.sql"); pub fn init(db_path: &Path) -> Result { let conn = Connection::open(db_path)?; configure(&conn)?; migrate(&conn)?; Ok(conn) } pub fn init_in_memory() -> Result { let conn = Connection::open_in_memory()?; configure(&conn)?; migrate(&conn)?; Ok(conn) } fn configure(conn: &Connection) -> Result<()> { conn.pragma_update(None, "journal_mode", "wal")?; conn.pragma_update(None, "foreign_keys", "ON")?; Ok(()) } fn migrate(conn: &Connection) -> Result<()> { let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; if version < 1 { conn.execute_batch(MIGRATION_001)?; conn.pragma_update(None, "user_version", 1)?; } Ok(()) } ``` - [x] **Step 5: Run tests to verify they pass** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo test db::tests ``` Expected: 3 tests pass. Note: `test_init_in_memory_sets_wal_mode` will return `"memory"` for in-memory DBs. Update the test: Replace the WAL test assertion: ```rust #[test] fn test_init_in_memory_sets_wal_mode() { // WAL is set but in-memory DBs report "memory" — verify no error on configure let conn = init_in_memory().expect("should initialize"); let mode: String = conn .query_row("PRAGMA journal_mode", [], |row| row.get(0)) .unwrap(); // In-memory databases report "memory" instead of "wal" assert_eq!(mode, "memory"); } ``` Re-run tests. Expected: 3 pass. - [x] **Step 6: Create error type** Create `src-tauri/src/error.rs`: ```rust use serde::Serialize; #[derive(Debug, Serialize)] pub struct AppError { pub message: String, } impl From for AppError { fn from(e: rusqlite::Error) -> Self { AppError { message: e.to_string(), } } } impl From for AppError { fn from(e: std::io::Error) -> Self { AppError { message: e.to_string(), } } } impl From for AppError { fn from(s: String) -> Self { AppError { message: s } } } impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } ``` - [x] **Step 7: Wire up db module in lib.rs** Replace `src-tauri/src/lib.rs`: ```rust mod db; mod error; use std::sync::Mutex; pub struct AppState { pub db: Mutex, } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { let db_dir = app.path().app_data_dir()?; std::fs::create_dir_all(&db_dir)?; let db_path = db_dir.join("orchai.db"); let conn = db::init(&db_path).expect("Failed to initialize database"); app.manage(AppState { db: Mutex::new(conn), }); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` - [x] **Step 8: Verify it compiles and runs** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo build ``` Expected: compiles with no errors. - [x] **Step 9: Commit** ```bash git add -A git commit -m "feat: SQLite database with migration system and full schema" ``` --- ### Task 4: Project model + CRUD repository + tests **Files:** - Create: `src-tauri/src/models/mod.rs` - Create: `src-tauri/src/models/project.rs` - Modify: `src-tauri/src/lib.rs` (add `mod models`) - [x] **Step 1: Write failing tests for Project CRUD** Create `src-tauri/src/models/mod.rs`: ```rust pub mod project; ``` Create `src-tauri/src/models/project.rs`: ```rust use rusqlite::{params, Connection, Result}; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { pub id: String, pub name: String, pub path: String, pub cloned_from: Option, pub base_branch: String, pub created_at: String, } impl Project { pub fn insert( conn: &Connection, name: &str, path: &str, cloned_from: Option<&str>, base_branch: &str, ) -> Result { todo!() } pub fn list(conn: &Connection) -> Result> { todo!() } pub fn get_by_id(conn: &Connection, id: &str) -> Result { todo!() } pub fn update(conn: &Connection, id: &str, name: &str, base_branch: &str) -> Result<()> { todo!() } pub fn delete(conn: &Connection, id: &str) -> Result<()> { todo!() } } #[cfg(test)] mod tests { use super::*; use crate::db; fn setup() -> Connection { db::init_in_memory().expect("db init should succeed") } #[test] fn test_insert_project_local_path() { let conn = setup(); let project = Project::insert(&conn, "My Project", "/home/user/code/myproject", None, "main") .expect("insert should succeed"); assert_eq!(project.name, "My Project"); assert_eq!(project.path, "/home/user/code/myproject"); assert!(project.cloned_from.is_none()); assert_eq!(project.base_branch, "main"); assert!(!project.id.is_empty()); assert!(!project.created_at.is_empty()); } #[test] fn test_insert_project_cloned() { let conn = setup(); let project = Project::insert( &conn, "Cloned Project", "/home/user/code/cloned", Some("https://github.com/org/repo.git"), "stable", ) .expect("insert should succeed"); assert_eq!(project.cloned_from.as_deref(), Some("https://github.com/org/repo.git")); assert_eq!(project.base_branch, "stable"); } #[test] fn test_list_projects_empty() { let conn = setup(); let projects = Project::list(&conn).expect("list should succeed"); assert!(projects.is_empty()); } #[test] fn test_list_projects_returns_all() { let conn = setup(); Project::insert(&conn, "A", "/path/a", None, "main").unwrap(); Project::insert(&conn, "B", "/path/b", None, "main").unwrap(); let projects = Project::list(&conn).expect("list should succeed"); assert_eq!(projects.len(), 2); } #[test] fn test_get_by_id() { let conn = setup(); let created = Project::insert(&conn, "Test", "/path/test", None, "main").unwrap(); let found = Project::get_by_id(&conn, &created.id).expect("get should succeed"); assert_eq!(found.id, created.id); assert_eq!(found.name, "Test"); } #[test] fn test_get_by_id_not_found() { let conn = setup(); let result = Project::get_by_id(&conn, "nonexistent"); assert!(result.is_err()); } #[test] fn test_update_project() { let conn = setup(); let created = Project::insert(&conn, "Old Name", "/path", None, "main").unwrap(); Project::update(&conn, &created.id, "New Name", "develop").expect("update should succeed"); let updated = Project::get_by_id(&conn, &created.id).unwrap(); assert_eq!(updated.name, "New Name"); assert_eq!(updated.base_branch, "develop"); } #[test] fn test_delete_project() { let conn = setup(); let created = Project::insert(&conn, "ToDelete", "/path", None, "main").unwrap(); Project::delete(&conn, &created.id).expect("delete should succeed"); let result = Project::get_by_id(&conn, &created.id); assert!(result.is_err()); } } ``` Add `mod models;` to `src-tauri/src/lib.rs` (after `mod error;`). - [x] **Step 2: Run tests to verify they fail** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo test models::project::tests ``` Expected: 8 failures with `not yet implemented`. - [x] **Step 3: Implement Project CRUD** Replace the `todo!()` implementations in `src-tauri/src/models/project.rs`: ```rust pub fn insert( conn: &Connection, name: &str, path: &str, cloned_from: Option<&str>, base_branch: &str, ) -> Result { let id = Uuid::new_v4().to_string(); let now = chrono::Utc::now().to_rfc3339(); conn.execute( "INSERT INTO projects (id, name, path, cloned_from, base_branch, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![id, name, path, cloned_from, base_branch, now], )?; Ok(Project { id, name: name.to_string(), path: path.to_string(), cloned_from: cloned_from.map(String::from), base_branch: base_branch.to_string(), created_at: now, }) } pub fn list(conn: &Connection) -> Result> { let mut stmt = conn.prepare( "SELECT id, name, path, cloned_from, base_branch, created_at FROM projects ORDER BY created_at DESC", )?; let rows = stmt.query_map([], |row| { Ok(Project { id: row.get(0)?, name: row.get(1)?, path: row.get(2)?, cloned_from: row.get(3)?, base_branch: row.get(4)?, created_at: row.get(5)?, }) })?; rows.collect() } pub fn get_by_id(conn: &Connection, id: &str) -> Result { conn.query_row( "SELECT id, name, path, cloned_from, base_branch, created_at FROM projects WHERE id = ?1", params![id], |row| { Ok(Project { id: row.get(0)?, name: row.get(1)?, path: row.get(2)?, cloned_from: row.get(3)?, base_branch: row.get(4)?, created_at: row.get(5)?, }) }, ) } pub fn update(conn: &Connection, id: &str, name: &str, base_branch: &str) -> Result<()> { conn.execute( "UPDATE projects SET name = ?1, base_branch = ?2 WHERE id = ?3", params![name, base_branch, id], )?; Ok(()) } pub fn delete(conn: &Connection, id: &str) -> Result<()> { conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?; Ok(()) } ``` Add the missing imports at the top of the file: ```rust use chrono; ``` - [x] **Step 4: Run tests to verify they pass** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo test models::project::tests ``` Expected: 8 tests pass. - [x] **Step 5: Commit** ```bash git add -A git commit -m "feat: Project model with CRUD operations and tests" ``` --- ### Task 5: Project Tauri commands **Files:** - Create: `src-tauri/src/commands/mod.rs` - Create: `src-tauri/src/commands/project.rs` - Modify: `src-tauri/src/lib.rs` - Modify: `src-tauri/Cargo.toml` (add tauri-plugin-dialog) - [x] **Step 1: Add dialog plugin dependency** Add to `src-tauri/Cargo.toml` under `[dependencies]`: ```toml tauri-plugin-dialog = "2" ``` Add to the `capabilities/default.json` permissions array: ```json "dialog:default" ``` - [x] **Step 2: Create commands module** Create `src-tauri/src/commands/mod.rs`: ```rust pub mod project; ``` Create `src-tauri/src/commands/project.rs`: ```rust use crate::error::AppError; use crate::models::project::Project; use crate::AppState; use std::process::Command; use tauri::State; #[tauri::command] pub fn create_project( state: State<'_, AppState>, name: String, path_or_url: String, base_branch: String, ) -> Result { let is_url = path_or_url.starts_with("http://") || path_or_url.starts_with("https://") || path_or_url.starts_with("git@"); let (local_path, cloned_from) = if is_url { let home = dirs::home_dir().ok_or_else(|| AppError::from("Cannot determine home directory".to_string()))?; let clone_dir = home.join("orchai-repos").join(&name); std::fs::create_dir_all(&clone_dir)?; let output = Command::new("git") .args(["clone", &path_or_url, clone_dir.to_str().unwrap()]) .output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(AppError::from(format!("git clone failed: {}", stderr))); } (clone_dir.to_string_lossy().to_string(), Some(path_or_url)) } else { let path = std::path::Path::new(&path_or_url); if !path.exists() { return Err(AppError::from(format!("Path does not exist: {}", path_or_url))); } if !path.join(".git").exists() { return Err(AppError::from(format!("Not a git repository: {}", path_or_url))); } (path_or_url, None) }; let db = state.db.lock().unwrap(); let project = Project::insert(&db, &name, &local_path, cloned_from.as_deref(), &base_branch)?; Ok(project) } #[tauri::command] pub fn list_projects(state: State<'_, AppState>) -> Result, AppError> { let db = state.db.lock().unwrap(); let projects = Project::list(&db)?; Ok(projects) } #[tauri::command] pub fn get_project(state: State<'_, AppState>, id: String) -> Result { let db = state.db.lock().unwrap(); let project = Project::get_by_id(&db, &id)?; Ok(project) } #[tauri::command] pub fn update_project( state: State<'_, AppState>, id: String, name: String, base_branch: String, ) -> Result<(), AppError> { let db = state.db.lock().unwrap(); Project::update(&db, &id, &name, &base_branch)?; Ok(()) } #[tauri::command] pub fn delete_project(state: State<'_, AppState>, id: String) -> Result<(), AppError> { let db = state.db.lock().unwrap(); Project::delete(&db, &id)?; Ok(()) } ``` - [x] **Step 3: Add `dirs` dependency** Add to `src-tauri/Cargo.toml` under `[dependencies]`: ```toml dirs = "5" ``` - [x] **Step 4: Wire up commands in lib.rs** Replace `src-tauri/src/lib.rs`: ```rust mod commands; mod db; mod error; mod models; use std::sync::Mutex; pub struct AppState { pub db: Mutex, } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .setup(|app| { let db_dir = app.path().app_data_dir()?; std::fs::create_dir_all(&db_dir)?; let db_path = db_dir.join("orchai.db"); let conn = db::init(&db_path).expect("Failed to initialize database"); app.manage(AppState { db: Mutex::new(conn), }); Ok(()) }) .invoke_handler(tauri::generate_handler![ commands::project::create_project, commands::project::list_projects, commands::project::get_project, commands::project::update_project, commands::project::delete_project, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` - [x] **Step 5: Verify it compiles** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo build ``` Expected: compiles with no errors. - [x] **Step 6: Commit** ```bash git add -A git commit -m "feat: Tauri commands for project CRUD with git clone support" ``` --- ### Task 6: TypeScript types + Tauri API wrappers **Files:** - Create: `src/lib/types.ts` - Create: `src/lib/api.ts` - [x] **Step 1: Install frontend dependencies** ```bash cd /home/leclere/Projets/orchai npm install react-router-dom npm install @tauri-apps/plugin-dialog ``` - [x] **Step 2: Create TypeScript types** Create `src/lib/types.ts`: ```typescript export interface Project { id: string; name: string; path: string; cloned_from: string | null; base_branch: string; created_at: string; } ``` - [x] **Step 3: Create API wrapper** Create `src/lib/api.ts`: ```typescript import { invoke } from "@tauri-apps/api/core"; import type { Project } from "./types"; export async function createProject( name: string, pathOrUrl: string, baseBranch: string ): Promise { return invoke("create_project", { name, pathOrUrl, baseBranch, }); } export async function listProjects(): Promise { return invoke("list_projects"); } export async function getProject(id: string): Promise { return invoke("get_project", { id }); } export async function updateProject( id: string, name: string, baseBranch: string ): Promise { return invoke("update_project", { id, name, baseBranch }); } export async function deleteProject(id: string): Promise { return invoke("delete_project", { id }); } ``` - [x] **Step 4: Commit** ```bash git add -A git commit -m "feat: TypeScript types and Tauri API wrappers for project CRUD" ``` --- ### Task 7: React app shell with router + layout **Files:** - Modify: `src/App.tsx` - Create: `src/components/layout/AppLayout.tsx` - Create: `src/components/layout/Sidebar.tsx` - [x] **Step 1: Create Sidebar component** Create directory structure: ```bash mkdir -p src/components/layout src/components/projects ``` Create `src/components/layout/Sidebar.tsx`: ```tsx import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { listProjects } from "../../lib/api"; import type { Project } from "../../lib/types"; export default function Sidebar() { const [projects, setProjects] = useState([]); const { projectId } = useParams(); useEffect(() => { listProjects().then(setProjects); }, []); // Expose a refresh function via custom event useEffect(() => { const handler = () => { listProjects().then(setProjects); }; window.addEventListener("orchai:refresh-projects", handler); return () => window.removeEventListener("orchai:refresh-projects", handler); }, []); return ( ); } ``` - [x] **Step 2: Create AppLayout component** Create `src/components/layout/AppLayout.tsx`: ```tsx import { Outlet } from "react-router-dom"; import Sidebar from "./Sidebar"; export default function AppLayout() { return (
); } ``` - [x] **Step 3: Set up router in App.tsx** Replace `src/App.tsx`: ```tsx import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import AppLayout from "./components/layout/AppLayout"; function EmptyState() { return (

Select a project or create a new one

); } function App() { return ( }> } /> Create project (coming next)} /> Project dashboard (coming next)} /> Edit project (coming next)} /> } /> ); } export default App; ``` - [x] **Step 4: Verify the shell renders** ```bash npm run tauri dev ``` Expected: window opens with dark sidebar on the left showing "Orchai" header, "Projects" section with "No projects yet" message, and a "+" button. Main area shows "Select a project or create a new one". Close after verifying. - [x] **Step 5: Commit** ```bash git add -A git commit -m "feat: React app shell with router, sidebar layout" ``` --- ### Task 8: Project list + create/edit form **Files:** - Create: `src/components/projects/ProjectForm.tsx` - Create: `src/components/projects/ProjectDashboard.tsx` - Modify: `src/App.tsx` - [x] **Step 1: Create ProjectForm component** Create `src/components/projects/ProjectForm.tsx`: ```tsx import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { open } from "@tauri-apps/plugin-dialog"; import { createProject, getProject, updateProject } from "../../lib/api"; export default function ProjectForm() { const navigate = useNavigate(); const { projectId } = useParams(); const isEditing = Boolean(projectId); const [name, setName] = useState(""); const [pathOrUrl, setPathOrUrl] = useState(""); const [baseBranch, setBaseBranch] = useState("main"); const [mode, setMode] = useState<"local" | "clone">("local"); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); useEffect(() => { if (projectId) { getProject(projectId).then((project) => { setName(project.name); setPathOrUrl(project.path); setBaseBranch(project.base_branch); if (project.cloned_from) { setMode("clone"); } }); } }, [projectId]); async function handleBrowse() { const selected = await open({ directory: true, multiple: false }); if (selected) { setPathOrUrl(selected as string); } } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); setLoading(true); try { if (isEditing && projectId) { await updateProject(projectId, name, baseBranch); } else { await createProject(name, pathOrUrl, baseBranch); } window.dispatchEvent(new Event("orchai:refresh-projects")); navigate("/"); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); setError(message); } finally { setLoading(false); } } return (

{isEditing ? "Edit project" : "New project"}

setName(e.target.value)} required className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
{!isEditing && ( <>
setPathOrUrl(e.target.value)} required placeholder={ mode === "local" ? "/home/user/code/myproject" : "https://github.com/org/repo.git" } className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> {mode === "local" && ( )}
)}
setBaseBranch(e.target.value)} required className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
{error && (
{error}
)}
); } ``` - [x] **Step 2: Create ProjectDashboard placeholder** Create `src/components/projects/ProjectDashboard.tsx`: ```tsx import { useEffect, useState } from "react"; import { useParams, Link, useNavigate } from "react-router-dom"; import { getProject, deleteProject } from "../../lib/api"; import type { Project } from "../../lib/types"; export default function ProjectDashboard() { const { projectId } = useParams(); const navigate = useNavigate(); const [project, setProject] = useState(null); useEffect(() => { if (projectId) { getProject(projectId).then(setProject); } }, [projectId]); async function handleDelete() { if (!projectId) return; if (!window.confirm(`Delete project "${project?.name}"?`)) return; await deleteProject(projectId); window.dispatchEvent(new Event("orchai:refresh-projects")); navigate("/"); } if (!project) { return
Loading...
; } return (

{project.name}

Edit
Path: {project.path}
{project.cloned_from && (
Cloned from: {project.cloned_from}
)}
Base branch: {project.base_branch}
Created: {new Date(project.created_at).toLocaleDateString()}
Tracker surveillance and ticket processing will be available in the next update.
); } ``` - [x] **Step 3: Wire up routes in App.tsx** Replace `src/App.tsx`: ```tsx import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import AppLayout from "./components/layout/AppLayout"; import ProjectForm from "./components/projects/ProjectForm"; import ProjectDashboard from "./components/projects/ProjectDashboard"; function EmptyState() { return (

Select a project or create a new one

); } function App() { return ( }> } /> } /> } /> } /> } /> ); } export default App; ``` - [x] **Step 4: Verify the full flow in the browser** ```bash npm run tauri dev ``` Test the following: 1. Click "+" in the sidebar to navigate to the create form 2. Fill in a name, select "Local folder", browse to an existing git repo, set base branch 3. Click "Create" -- project appears in sidebar 4. Click the project in sidebar -- dashboard shows project details 5. Click "Edit" -- form pre-fills with project data 6. Click "Delete" -- project removed from sidebar - [x] **Step 5: Commit** ```bash git add -A git commit -m "feat: project create/edit/delete UI with folder picker and git clone" ``` --- ### Task 9: Final verification + cleanup **Files:** - Verify all tests pass - Clean up any scaffold files not needed - [x] **Step 1: Run all Rust tests** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo test ``` Expected: all tests pass (8 model tests + 3 db tests = 11 tests). - [x] **Step 2: Run Rust clippy** ```bash cd /home/leclere/Projets/orchai/src-tauri cargo clippy -- -D warnings ``` Expected: no warnings. If there are warnings, fix them. - [x] **Step 3: Verify frontend builds** ```bash cd /home/leclere/Projets/orchai npm run build ``` Expected: Vite build succeeds. - [x] **Step 4: Clean up scaffold files** Remove any remaining scaffold assets that are not needed: - `src/assets/react.svg` (if still present) - Any other default scaffold content - [x] **Step 5: Final integration test** ```bash npm run tauri dev ``` Test the complete flow one more time: 1. Create a project pointing to a local git repo 2. Verify it appears in sidebar 3. View project dashboard 4. Edit project name and base branch 5. Delete the project 6. Verify sidebar is empty again - [x] **Step 6: Commit** ```bash git add -A git commit -m "cleanup: remove scaffold assets, verify all tests pass" ```