# 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 (
    <div className="min-h-screen bg-gray-50 text-gray-900">
      <h1 className="text-2xl font-bold p-8">Orchai</h1>
    </div>
  );
}

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<Connection> {
    todo!()
}

pub fn init_in_memory() -> Result<Connection> {
    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<String> = 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::<Result<Vec<String>, _>>()
            .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<Connection> {
    let conn = Connection::open(db_path)?;
    configure(&conn)?;
    migrate(&conn)?;
    Ok(conn)
}

pub fn init_in_memory() -> Result<Connection> {
    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<rusqlite::Error> for AppError {
    fn from(e: rusqlite::Error) -> Self {
        AppError {
            message: e.to_string(),
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError {
            message: e.to_string(),
        }
    }
}

impl From<String> 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<rusqlite::Connection>,
}

#[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<String>,
    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<Project> {
        todo!()
    }

    pub fn list(conn: &Connection) -> Result<Vec<Project>> {
        todo!()
    }

    pub fn get_by_id(conn: &Connection, id: &str) -> Result<Project> {
        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<Project> {
        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<Vec<Project>> {
        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<Project> {
        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<Project, AppError> {
    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<Vec<Project>, 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<Project, AppError> {
    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<rusqlite::Connection>,
}

#[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<Project> {
  return invoke("create_project", {
    name,
    pathOrUrl,
    baseBranch,
  });
}

export async function listProjects(): Promise<Project[]> {
  return invoke("list_projects");
}

export async function getProject(id: string): Promise<Project> {
  return invoke("get_project", { id });
}

export async function updateProject(
  id: string,
  name: string,
  baseBranch: string
): Promise<void> {
  return invoke("update_project", { id, name, baseBranch });
}

export async function deleteProject(id: string): Promise<void> {
  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<Project[]>([]);
  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 (
    <aside className="w-64 bg-gray-900 text-gray-100 flex flex-col h-screen">
      <div className="p-4 border-b border-gray-700">
        <h1 className="text-lg font-bold">Orchai</h1>
      </div>

      <nav className="flex-1 overflow-y-auto p-2">
        <div className="flex items-center justify-between px-2 py-1 mb-1">
          <span className="text-xs font-semibold text-gray-400 uppercase tracking-wider">
            Projects
          </span>
          <Link
            to="/projects/new"
            className="text-gray-400 hover:text-white text-lg leading-none"
            title="Add project"
          >
            +
          </Link>
        </div>

        {projects.map((project) => (
          <Link
            key={project.id}
            to={`/projects/${project.id}`}
            className={`block px-3 py-2 rounded text-sm ${
              projectId === project.id
                ? "bg-gray-700 text-white"
                : "text-gray-300 hover:bg-gray-800 hover:text-white"
            }`}
          >
            {project.name}
          </Link>
        ))}

        {projects.length === 0 && (
          <p className="px-3 py-2 text-sm text-gray-500">No projects yet</p>
        )}
      </nav>
    </aside>
  );
}
```

- [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 (
    <div className="flex h-screen">
      <Sidebar />
      <main className="flex-1 overflow-y-auto bg-gray-50">
        <Outlet />
      </main>
    </div>
  );
}
```

- [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 (
    <div className="flex items-center justify-center h-full text-gray-400">
      <p>Select a project or create a new one</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<AppLayout />}>
          <Route index element={<EmptyState />} />
          <Route path="/projects/new" element={<div className="p-8">Create project (coming next)</div>} />
          <Route path="/projects/:projectId" element={<div className="p-8">Project dashboard (coming next)</div>} />
          <Route path="/projects/:projectId/edit" element={<div className="p-8">Edit project (coming next)</div>} />
          <Route path="*" element={<Navigate to="/" replace />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

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<string | null>(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 (
    <div className="max-w-lg mx-auto p-8">
      <h2 className="text-xl font-bold mb-6">
        {isEditing ? "Edit project" : "New project"}
      </h2>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">
            Project name
          </label>
          <input
            type="text"
            value={name}
            onChange={(e) => 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"
          />
        </div>

        {!isEditing && (
          <>
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Source
              </label>
              <div className="flex gap-4">
                <label className="flex items-center gap-1 text-sm">
                  <input
                    type="radio"
                    checked={mode === "local"}
                    onChange={() => setMode("local")}
                  />
                  Local folder
                </label>
                <label className="flex items-center gap-1 text-sm">
                  <input
                    type="radio"
                    checked={mode === "clone"}
                    onChange={() => setMode("clone")}
                  />
                  Clone from URL
                </label>
              </div>
            </div>

            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                {mode === "local" ? "Folder path" : "Git URL"}
              </label>
              <div className="flex gap-2">
                <input
                  type="text"
                  value={pathOrUrl}
                  onChange={(e) => 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" && (
                  <button
                    type="button"
                    onClick={handleBrowse}
                    className="px-3 py-2 bg-gray-200 rounded text-sm hover:bg-gray-300"
                  >
                    Browse
                  </button>
                )}
              </div>
            </div>
          </>
        )}

        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">
            Base branch
          </label>
          <input
            type="text"
            value={baseBranch}
            onChange={(e) => 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"
          />
        </div>

        {error && (
          <div className="text-red-600 text-sm bg-red-50 border border-red-200 rounded p-2">
            {error}
          </div>
        )}

        <div className="flex gap-2">
          <button
            type="submit"
            disabled={loading}
            className="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? "Saving..." : isEditing ? "Save" : "Create"}
          </button>
          <button
            type="button"
            onClick={() => navigate(-1)}
            className="px-4 py-2 bg-gray-200 rounded text-sm hover:bg-gray-300"
          >
            Cancel
          </button>
        </div>
      </form>
    </div>
  );
}
```

- [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<Project | null>(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 <div className="p-8 text-gray-400">Loading...</div>;
  }

  return (
    <div className="p-8">
      <div className="flex items-center justify-between mb-6">
        <h2 className="text-xl font-bold">{project.name}</h2>
        <div className="flex gap-2">
          <Link
            to={`/projects/${project.id}/edit`}
            className="px-3 py-1 bg-gray-200 rounded text-sm hover:bg-gray-300"
          >
            Edit
          </Link>
          <button
            onClick={handleDelete}
            className="px-3 py-1 bg-red-100 text-red-700 rounded text-sm hover:bg-red-200"
          >
            Delete
          </button>
        </div>
      </div>

      <div className="bg-white rounded-lg border border-gray-200 p-4 space-y-3">
        <div>
          <span className="text-sm text-gray-500">Path:</span>
          <span className="ml-2 text-sm font-mono">{project.path}</span>
        </div>
        {project.cloned_from && (
          <div>
            <span className="text-sm text-gray-500">Cloned from:</span>
            <span className="ml-2 text-sm font-mono">{project.cloned_from}</span>
          </div>
        )}
        <div>
          <span className="text-sm text-gray-500">Base branch:</span>
          <span className="ml-2 text-sm font-mono">{project.base_branch}</span>
        </div>
        <div>
          <span className="text-sm text-gray-500">Created:</span>
          <span className="ml-2 text-sm">{new Date(project.created_at).toLocaleDateString()}</span>
        </div>
      </div>

      <div className="mt-8 text-gray-400 text-sm">
        Tracker surveillance and ticket processing will be available in the next update.
      </div>
    </div>
  );
}
```

- [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 (
    <div className="flex items-center justify-center h-full text-gray-400">
      <p>Select a project or create a new one</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<AppLayout />}>
          <Route index element={<EmptyState />} />
          <Route path="/projects/new" element={<ProjectForm />} />
          <Route path="/projects/:projectId" element={<ProjectDashboard />} />
          <Route path="/projects/:projectId/edit" element={<ProjectForm />} />
          <Route path="*" element={<Navigate to="/" replace />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

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"
```
