2026-04-13 07:13:05 +00:00
# Orchai Phase 1: Foundation Implementation Plan
2026-04-14 07:02:04 +00:00
> **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.
2026-04-13 07:13:05 +00:00
**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/`
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Save existing repo contents**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets
cp -r orchai/docs /tmp/orchai-docs-backup
cp -r orchai/.git /tmp/orchai-git-backup
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Scaffold Tauri 2 project**
2026-04-13 07:13:05 +00:00
```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`
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Restore repo history and docs**
2026-04-13 07:13:05 +00:00
```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
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Install dependencies and verify**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai
npm install
cd src-tauri & & cargo build
cd ..
```
Expected: build succeeds with no errors.
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Verify dev server starts**
2026-04-13 07:13:05 +00:00
```bash
npm run tauri dev
```
Expected: Tauri window opens with the default React starter page. Close it after verifying.
2026-04-14 07:02:04 +00:00
- [x] **Step 6: Commit**
2026-04-13 07:13:05 +00:00
```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`
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Install Tailwind**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai
npm install -D tailwindcss @tailwindcss/vite
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Add Tailwind to Vite config**
2026-04-13 07:13:05 +00:00
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/**"],
},
},
}));
```
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Replace index.css with Tailwind directives**
2026-04-13 07:13:05 +00:00
Replace the contents of `src/index.css` :
```css
@import "tailwindcss";
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Delete App.css and clean up App.tsx**
2026-04-13 07:13:05 +00:00
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;
```
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Update Tauri config**
2026-04-13 07:13:05 +00:00
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
}
}
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 6: Verify Tailwind works**
2026-04-13 07:13:05 +00:00
```bash
npm run tauri dev
```
Expected: window opens, "Orchai" heading rendered with Tailwind styling (bold, large). Close after verifying.
2026-04-14 07:02:04 +00:00
- [x] **Step 7: Commit**
2026-04-13 07:13:05 +00:00
```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`
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Write the failing test for db initialization**
2026-04-13 07:13:05 +00:00
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");
}
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Run tests to verify they fail**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo test db::tests
```
Expected: 3 failures with `not yet implemented` .
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Create migration SQL**
2026-04-13 07:13:05 +00:00
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'))
);
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Implement db::init and db::init_in_memory**
2026-04-13 07:13:05 +00:00
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(())
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Run tests to verify they pass**
2026-04-13 07:13:05 +00:00
```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.
2026-04-14 07:02:04 +00:00
- [x] **Step 6: Create error type**
2026-04-13 07:13:05 +00:00
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)
}
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 7: Wire up db module in lib.rs**
2026-04-13 07:13:05 +00:00
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");
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 8: Verify it compiles and runs**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo build
```
Expected: compiles with no errors.
2026-04-14 07:02:04 +00:00
- [x] **Step 9: Commit**
2026-04-13 07:13:05 +00:00
```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` )
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Write failing tests for Project CRUD**
2026-04-13 07:13:05 +00:00
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;` ).
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Run tests to verify they fail**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo test models::project::tests
```
Expected: 8 failures with `not yet implemented` .
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Implement Project CRUD**
2026-04-13 07:13:05 +00:00
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;
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Run tests to verify they pass**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo test models::project::tests
```
Expected: 8 tests pass.
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Commit**
2026-04-13 07:13:05 +00:00
```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)
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Add dialog plugin dependency**
2026-04-13 07:13:05 +00:00
Add to `src-tauri/Cargo.toml` under `[dependencies]` :
```toml
tauri-plugin-dialog = "2"
```
Add to the `capabilities/default.json` permissions array:
```json
"dialog:default"
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Create commands module**
2026-04-13 07:13:05 +00:00
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(())
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Add `dirs` dependency**
2026-04-13 07:13:05 +00:00
Add to `src-tauri/Cargo.toml` under `[dependencies]` :
```toml
dirs = "5"
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Wire up commands in lib.rs**
2026-04-13 07:13:05 +00:00
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");
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Verify it compiles**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo build
```
Expected: compiles with no errors.
2026-04-14 07:02:04 +00:00
- [x] **Step 6: Commit**
2026-04-13 07:13:05 +00:00
```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`
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Install frontend dependencies**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai
npm install react-router-dom
npm install @tauri -apps/plugin-dialog
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Create TypeScript types**
2026-04-13 07:13:05 +00:00
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;
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Create API wrapper**
2026-04-13 07:13:05 +00:00
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 });
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Commit**
2026-04-13 07:13:05 +00:00
```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`
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Create Sidebar component**
2026-04-13 07:13:05 +00:00
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 >
);
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Create AppLayout component**
2026-04-13 07:13:05 +00:00
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 >
);
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Set up router in App.tsx**
2026-04-13 07:13:05 +00:00
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;
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Verify the shell renders**
2026-04-13 07:13:05 +00:00
```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.
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Commit**
2026-04-13 07:13:05 +00:00
```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`
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Create ProjectForm component**
2026-04-13 07:13:05 +00:00
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 >
);
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Create ProjectDashboard placeholder**
2026-04-13 07:13:05 +00:00
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 >
);
}
```
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Wire up routes in App.tsx**
2026-04-13 07:13:05 +00:00
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;
```
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Verify the full flow in the browser**
2026-04-13 07:13:05 +00:00
```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
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Commit**
2026-04-13 07:13:05 +00:00
```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
2026-04-14 07:02:04 +00:00
- [x] **Step 1: Run all Rust tests**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo test
```
Expected: all tests pass (8 model tests + 3 db tests = 11 tests).
2026-04-14 07:02:04 +00:00
- [x] **Step 2: Run Rust clippy**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai/src-tauri
cargo clippy -- -D warnings
```
Expected: no warnings. If there are warnings, fix them.
2026-04-14 07:02:04 +00:00
- [x] **Step 3: Verify frontend builds**
2026-04-13 07:13:05 +00:00
```bash
cd /home/leclere/Projets/orchai
npm run build
```
Expected: Vite build succeeds.
2026-04-14 07:02:04 +00:00
- [x] **Step 4: Clean up scaffold files**
2026-04-13 07:13:05 +00:00
Remove any remaining scaffold assets that are not needed:
- `src/assets/react.svg` (if still present)
- Any other default scaffold content
2026-04-14 07:02:04 +00:00
- [x] **Step 5: Final integration test**
2026-04-13 07:13:05 +00:00
```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
2026-04-14 07:02:04 +00:00
- [x] **Step 6: Commit**
2026-04-13 07:13:05 +00:00
```bash
git add -A
git commit -m "cleanup: remove scaffold assets, verify all tests pass"
```