# 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 (`- [ ]`) 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/`
- [ ] **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
```
- [ ] **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`
- [ ] **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
```
- [ ] **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.
- [ ] **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.
- [ ] **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`
- [ ] **Step 1: Install Tailwind**
```bash
cd /home/leclere/Projets/orchai
npm install -D tailwindcss @tailwindcss/vite
```
- [ ] **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/**"],
},
},
}));
```
- [ ] **Step 3: Replace index.css with Tailwind directives**
Replace the contents of `src/index.css`:
```css
@import "tailwindcss";
```
- [ ] **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;
```
- [ ] **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
}
}
}
```
- [ ] **Step 6: Verify Tailwind works**
```bash
npm run tauri dev
```
Expected: window opens, "Orchai" heading rendered with Tailwind styling (bold, large). Close after verifying.
- [ ] **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`
- [ ] **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");
}
}
```
- [ ] **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`.
- [ ] **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'))
);
```
- [ ] **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(())
}
```
- [ ] **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.
- [ ] **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)
}
}
```
- [ ] **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");
}
```
- [ ] **Step 8: Verify it compiles and runs**
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo build
```
Expected: compiles with no errors.
- [ ] **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`)
- [ ] **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;`).
- [ ] **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`.
- [ ] **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;
```
- [ ] **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.
- [ ] **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)
- [ ] **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"
```
- [ ] **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(())
}
```
- [ ] **Step 3: Add `dirs` dependency**
Add to `src-tauri/Cargo.toml` under `[dependencies]`:
```toml
dirs = "5"
```
- [ ] **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");
}
```
- [ ] **Step 5: Verify it compiles**
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo build
```
Expected: compiles with no errors.
- [ ] **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`
- [ ] **Step 1: Install frontend dependencies**
```bash
cd /home/leclere/Projets/orchai
npm install react-router-dom
npm install @tauri-apps/plugin-dialog
```
- [ ] **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;
}
```
- [ ] **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 });
}
```
- [ ] **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`
- [ ] **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 (
Orchai
Projects
+
{projects.map((project) => (
{project.name}
))}
{projects.length === 0 && (
No projects yet
)}
);
}
```
- [ ] **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 (
);
}
```
- [ ] **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;
```
- [ ] **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.
- [ ] **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`
- [ ] **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"}
);
}
```
- [ ] **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
Delete
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.
);
}
```
- [ ] **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;
```
- [ ] **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
- [ ] **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
- [ ] **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).
- [ ] **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.
- [ ] **Step 3: Verify frontend builds**
```bash
cd /home/leclere/Projets/orchai
npm run build
```
Expected: Vite build succeeds.
- [ ] **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
- [ ] **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
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "cleanup: remove scaffold assets, verify all tests pass"
```