Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3be55795a6 | |||
| 2332b7f815 | |||
| 9736c42424 | |||
| 0638ce7c98 | |||
| 0660f52e2b | |||
| 33edbad713 | |||
| 307ae71857 | |||
| 55b3bee2c8 |
8
.cargo/config.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Build configuration for the IdeA workspace.
|
||||||
|
#
|
||||||
|
# Link the system libgit2 (via pkg-config) instead of vendoring/compiling it from
|
||||||
|
# source. The dev machines and CI provide libgit2 ≥ 1.9 (matching git2 0.20), and
|
||||||
|
# this avoids requiring cmake for the vendored build. Static vendoring for the
|
||||||
|
# portable AppImage is handled separately at packaging time (L11).
|
||||||
|
[env]
|
||||||
|
LIBGIT2_SYS_USE_PKG_CONFIG = "1"
|
||||||
1
.claude/worktrees/agent-a2650e91d2bd39ca2
Submodule
1
.claude/worktrees/agent-a4227a8f495123597
Submodule
1
.claude/worktrees/agent-a78df266a0bbaf5c3
Submodule
1
.claude/worktrees/agent-aeb1e862ef04b991b
Submodule
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# ─── Rust / Cargo ───────────────────────────────────────────────────────────
|
||||||
|
# Build output for the whole workspace (Cargo.lock IS committed — it's an app).
|
||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# ─── Node / frontend ────────────────────────────────────────────────────────
|
||||||
|
# Dependencies and build output (package-lock.json IS committed).
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
# npm/yarn/pnpm debug logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
.pnpm-store/
|
||||||
|
# Vite / vitest caches
|
||||||
|
frontend/.vite/
|
||||||
|
frontend/coverage/
|
||||||
|
|
||||||
|
# ─── Tauri ──────────────────────────────────────────────────────────────────
|
||||||
|
# Bundles live under target/ (already ignored). Generated icons are committed.
|
||||||
|
.tauri/
|
||||||
|
|
||||||
|
# ─── Claude Code ────────────────────────────────────────────────────────────
|
||||||
|
# Personal, machine-local overrides (shared settings.json, if any, stays tracked).
|
||||||
|
.claude/settings.local.json
|
||||||
|
# Ephemeral git worktrees created by Claude Code's isolated sub-agents — dev
|
||||||
|
# tooling only, unrelated to IdeA (which stays git-independent).
|
||||||
|
.claude/worktrees/
|
||||||
|
|
||||||
|
# ─── IdeA project data ──────────────────────────────────────────────────────
|
||||||
|
# Ephemeral per-agent run directories (isolated PTY cwd + generated convention
|
||||||
|
# files), created at activation — not versioned (ARCHITECTURE §9.1 / §14.1).
|
||||||
|
.ideai/run/
|
||||||
|
|
||||||
|
# ─── Editors / OS ───────────────────────────────────────────────────────────
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
19
.ideai/agents.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"agentId": "a6ced819-b893-4213-b003-9e9dc79b9641",
|
||||||
|
"name": "Main",
|
||||||
|
"mdPath": "agents/main.md",
|
||||||
|
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
||||||
|
"synchronized": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentId": "dce19c75-9669-4e45-b8de-9950025157da",
|
||||||
|
"name": "Architect",
|
||||||
|
"mdPath": "agents/architect.md",
|
||||||
|
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
||||||
|
"synchronized": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
601
.ideai/agents/architect.md
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
# IdeA — Cartographie d'Architecture
|
||||||
|
|
||||||
|
> Document de référence produit par l'**Agent Architecture**.
|
||||||
|
> Fait autorité sur les frontières, ports, adapters, modules et conventions.
|
||||||
|
> Toute feature DOIT être validée contre ce document avant développement.
|
||||||
|
> Architecture **Hexagonale (Ports & Adapters)** + **SOLID**, stricte.
|
||||||
|
>
|
||||||
|
> Stack non négociable : Tauri v2 (shell) · Rust (cœur hexagonal) · TypeScript + React (UI) · xterm.js + portable-pty (terminaux) · git2/libgit2 · russh/ssh2 · wsl.exe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Principes : SOLID + Hexagonal, appliqués concrètement
|
||||||
|
|
||||||
|
### 1.1 Règle de dépendance (la seule qui compte)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Le sens des dépendances │
|
||||||
|
│ │
|
||||||
|
Présentation ─► Application ─► Domaine ◄─ Infrastructure │
|
||||||
|
(React/Tauri) (use cases) (pur) (adapters) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Le Domaine ne dépend de RIEN** : ni Tauri, ni tokio, ni git2, ni portable-pty, ni serde (le moins possible — voir §1.4). Il ne contient que des entités, value objects, règles métier et **traits = ports**.
|
||||||
|
- **L'Application** dépend du Domaine. Elle orchestre les use cases en parlant **uniquement aux ports** (traits), jamais aux adapters concrets.
|
||||||
|
- **L'Infrastructure** dépend du Domaine et de l'Application (elle implémente les ports). Elle contient tous les détails techniques (PTY, FS, git, SSH, WSL, stores).
|
||||||
|
- **La Présentation** (Tauri commands + React) dépend de l'Application. Les commandes Tauri sont des **adapters entrants (driving adapters)** ; les impl de ports sont des **adapters sortants (driven adapters)**.
|
||||||
|
|
||||||
|
Aucune flèche ne pointe **vers** la présentation ou l'infrastructure. L'inversion de dépendance (le **D** de SOLID) est matérialisée par les traits définis dans le domaine et implémentés dehors.
|
||||||
|
|
||||||
|
### 1.2 SOLID, point par point, traduit IdeA
|
||||||
|
|
||||||
|
| Principe | Application concrète |
|
||||||
|
|---|---|
|
||||||
|
| **S** — Single Responsibility | Un use case = une intention métier (`LaunchAgent`, `SyncAgentWithTemplate`). Un adapter = une techno (`Git2Repository` ne fait que du git). Le `LayoutNode` ne gère que la topologie, pas le rendu. |
|
||||||
|
| **O** — Open/Closed | Ajouter une IA = ajouter un **profil déclaratif** (donnée), pas du code. Ajouter un mode distant = nouvel adapter `RemoteHost` sans toucher aux use cases. Ajouter une stratégie d'injection de contexte = nouvelle variante d'enum + handler, use case inchangé. |
|
||||||
|
| **L** — Liskov | Tout `RemoteHost` (local, SSH, WSL) est substituable : un use case marche identiquement quelle que soit l'impl. Les contrats (pré/postconditions) des ports sont documentés et respectés par chaque adapter. |
|
||||||
|
| **I** — Interface Segregation | Ports **fins et ciblés** : `ProcessSpawner`, `FileSystem`, `PtyPort` séparés plutôt qu'un `System` fourre-tout. Un use case ne reçoit que les ports qu'il consomme. |
|
||||||
|
| **D** — Dependency Inversion | Domaine définit les traits ; infra les implémente ; l'application reçoit des `Arc<dyn Port>` par **injection** (composition root dans la couche Tauri). |
|
||||||
|
|
||||||
|
### 1.3 Hexagonal côté Frontend (React aussi)
|
||||||
|
|
||||||
|
L'hexagonal ne s'arrête pas à Rust. Côté React on applique le même découpage :
|
||||||
|
|
||||||
|
- **Domaine UI / modèles de vue** : types TS purs (miroir des DTO), logique de présentation pure (ex. calcul de tailles de cellules d'un `LayoutNode`), testable sans React ni Tauri.
|
||||||
|
- **Ports UI** : interfaces TS (`AgentGateway`, `TerminalGateway`, `ProjectGateway`, `LayoutGateway`, `GitGateway`, `RemoteGateway`) décrivant **ce dont l'UI a besoin**, indépendamment du transport.
|
||||||
|
- **Adapters UI** : implémentation des ports via `@tauri-apps/api` (`invoke` pour commands, `listen` pour events). Remplaçables par des **mocks** en test/Storybook.
|
||||||
|
- **Présentation** : composants React, hooks, state (Zustand/Redux) qui consomment les ports UI, jamais `invoke()` en direct.
|
||||||
|
|
||||||
|
Bénéfice : le frontend est testable et développable sans backend (adapters mock), et la frontière IPC est centralisée en un seul endroit.
|
||||||
|
|
||||||
|
### 1.4 Domaine pur vs adapters — règle pratique Rust
|
||||||
|
|
||||||
|
- Le crate `domain` est **`#![no_std]`-friendly d'esprit** (pas imposé), sans dépendance I/O. Tolérance pragmatique : `serde` est autorisé **uniquement** pour dériver la (dé)sérialisation des entités persistées (manifeste, layout, profils), car c'est une contrainte métier de format, pas un détail technique d'I/O. Les **traits/ports** y vivent. Pas de `tokio`, pas de `std::process`, pas de `std::fs`.
|
||||||
|
- Tout ce qui touche le monde réel (`std::fs`, `Command`, sockets, libgit2, PTY) vit **exclusivement** dans `infrastructure`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Découpage en couches & frontière Rust ↔ Tauri ↔ React
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PRÉSENTATION (Frontend) — TypeScript + React + xterm.js │
|
||||||
|
│ features/* · ui-ports (gateways) · tauri-adapters (invoke/listen) │
|
||||||
|
└───────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ IPC Tauri (commands ⇄ events, JSON)
|
||||||
|
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||||
|
│ PRÉSENTATION (Backend) — crate `app-tauri` (DRIVING ADAPTER) │
|
||||||
|
│ #[tauri::command] handlers · event emitters · COMPOSITION ROOT (DI) │
|
||||||
|
│ PTY byte-stream bridge ⇄ xterm.js │
|
||||||
|
└───────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ appels de use cases (Arc<UseCase>)
|
||||||
|
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||||
|
│ APPLICATION — crate `application` │
|
||||||
|
│ Use cases / services · DTOs · orchestration · transactions métier │
|
||||||
|
│ Dépend UNIQUEMENT des ports (traits) du domaine │
|
||||||
|
└───────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ implémente / consomme
|
||||||
|
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||||
|
│ DOMAINE — crate `domain` (PUR, sans I/O) │
|
||||||
|
│ Entities · Value Objects · Invariants · PORTS (traits) · DomainEvents │
|
||||||
|
└───────────────────────────────▲───────────────────────────────────────┘
|
||||||
|
│ implémentent les ports (DRIVEN ADAPTERS)
|
||||||
|
┌───────────────────────────────┴───────────────────────────────────────┐
|
||||||
|
│ INFRASTRUCTURE — crate `infrastructure` │
|
||||||
|
│ portable-pty · git2 · russh/ssh2 · wsl.exe · fs local · md/json store │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontière IPC Tauri — deux directions
|
||||||
|
|
||||||
|
- **Commands (Frontend → Backend, request/response)** : `invoke("create_project", {...})`. Le handler `#[tauri::command]` désérialise le DTO, appelle le use case, renvoie un `Result<DTO, ErrorDTO>`. **Stateless** côté forme : tout l'état vit dans des services managés via `tauri::State`.
|
||||||
|
- **Events (Backend → Frontend, push)** : flux PTY (octets/base64), changements de statut d'agent, fin de processus, progrès git, drift de template détecté. Émis via `app_handle.emit(...)` / channels Tauri. L'`EventBus` domaine est relayé vers ces events Tauri par un adapter dans `app-tauri`.
|
||||||
|
|
||||||
|
> **Décision** : le flux PTY haute fréquence passe par des **Tauri Channels** (`tauri::ipc::Channel`) plutôt que des events globaux, pour la perf et l'isolement par session terminal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Modèle de domaine
|
||||||
|
|
||||||
|
### 3.1 Vue d'ensemble (relations)
|
||||||
|
|
||||||
|
```
|
||||||
|
Workspace 1───* Window 1───* Tab 1───1 Project
|
||||||
|
│ │
|
||||||
|
│ 1 ├──* Agent ─────? AgentTemplate (origine)
|
||||||
|
│ │ │ 1
|
||||||
|
│ 1 │ └──1 AgentProfile (runtime IA, par réf id)
|
||||||
|
LayoutTree ├──1 GitRepository
|
||||||
|
(LayoutNode récursif) ├──1 RemoteHost (Local | Ssh | Wsl)
|
||||||
|
│ feuilles └──1 AgentManifest (.ideai/agents.json)
|
||||||
|
▼
|
||||||
|
TerminalSession 1───? Agent (si lancé par un agent)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Entités & Value Objects (avec invariants)
|
||||||
|
|
||||||
|
**`ProjectId`, `AgentId`, `TemplateId`, `ProfileId`, `SessionId`, `WindowId`, `TabId`, `NodeId`** — VO `newtype(Uuid)` ou string typée. Invariant : non vide, immuable.
|
||||||
|
|
||||||
|
**`Project`** (entité, racine d'agrégat projet)
|
||||||
|
- Champs : `id`, `name`, `root: ProjectPath`, `remote: RemoteRef`, `created_at`.
|
||||||
|
- Invariants : `root` doit être un chemin **absolu et valide pour son `RemoteRef`** ; deux projets ne peuvent partager le même `(remote, root)`.
|
||||||
|
|
||||||
|
**`ProjectPath`** (VO) — chemin absolu normalisé, conscient de la plateforme cible (POSIX vs Windows vs WSL `/mnt/...`).
|
||||||
|
|
||||||
|
**`Agent`** (entité)
|
||||||
|
- Champs : `id`, `name`, `context: AgentContextRef` (chemin du `.md` dans `.ideai/`), `profile_id: ProfileId`, `origin: AgentOrigin` (`Scratch` | `FromTemplate { template_id, synced_version }`), `synchronized: bool`.
|
||||||
|
- Invariants : `synchronized == true` ⇒ `origin == FromTemplate{..}` (on ne peut pas synchroniser un agent créé from scratch). `context` doit exister à l'activation. `profile_id` doit référencer un `AgentProfile` connu.
|
||||||
|
|
||||||
|
**`AgentTemplate`** (entité, store global)
|
||||||
|
- Champs : `id`, `name`, `content_md: MarkdownDoc`, `version: TemplateVersion`, `default_profile_id`.
|
||||||
|
- Invariants : `version` **monotone croissante** ; toute modification du `content_md` ⇒ `version + 1` (voir §8).
|
||||||
|
|
||||||
|
**`AgentProfile`** (entité de config runtime IA — le port `AgentRuntime` est paramétré par elle)
|
||||||
|
- Champs : `id`, `name`, `command: String`, `args: Vec<String>`, `context_injection: ContextInjection`, `detect: Option<String>`, `cwd_template: String` (ex. `"{projectRoot}"`).
|
||||||
|
- Invariants : `command` non vide ; cohérence de `ContextInjection` (voir VO ci-dessous).
|
||||||
|
|
||||||
|
**`ContextInjection`** (VO, enum — cœur du moteur IA flexible)
|
||||||
|
```
|
||||||
|
ContextInjection =
|
||||||
|
| ConventionFile { target: String } // ex. "CLAUDE.md" / "AGENTS.md" / "GEMINI.md"
|
||||||
|
| Flag { flag: String } // ex. "--context-file {path}" ou "-f"
|
||||||
|
| Stdin // pipe du contenu md sur stdin
|
||||||
|
| Env { var: String } // ex. "AGENT_CONTEXT_FILE"
|
||||||
|
```
|
||||||
|
- Invariants : `ConventionFile.target` est un nom de fichier relatif (pas de `..`, pas absolu) ; `Env.var` est un identifiant d'env valide ; `Flag.flag` non vide.
|
||||||
|
|
||||||
|
**`TerminalSession`** (entité)
|
||||||
|
- Champs : `id`, `node_id` (cellule du layout qui l'héberge), `cwd: ProjectPath`, `kind: SessionKind` (`Plain` | `Agent { agent_id }`), `pty_size: PtySize { rows, cols }`, `status` (`Starting|Running|Exited{code}`).
|
||||||
|
- Invariants : une cellule (feuille de layout) héberge **au plus une** `TerminalSession` active. `pty_size.rows>0 && cols>0`.
|
||||||
|
|
||||||
|
**`LayoutNode` / `LayoutTree`** (VO récursif — voir §7 pour le détail complet)
|
||||||
|
- Invariants : poids relatifs strictement positifs ; somme normalisable ; pas de fusion qui chevauche deux conteneurs distincts ; un `Leaf` référence 0 ou 1 `SessionId`.
|
||||||
|
|
||||||
|
**`RemoteHost`** (VO de stratégie de localisation — abstrait Local/SSH/WSL)
|
||||||
|
```
|
||||||
|
RemoteRef =
|
||||||
|
| Local
|
||||||
|
| Ssh { host, port, user, auth: SshAuth, remote_root }
|
||||||
|
| Wsl { distro: String }
|
||||||
|
```
|
||||||
|
- Invariants : `Ssh.port` ∈ 1..=65535 ; `Wsl.distro` non vide ; pour `Ssh`/`Wsl`, les chemins projet sont interprétés côté distant.
|
||||||
|
|
||||||
|
**`GitRepository`** (entité)
|
||||||
|
- Champs : `project_id`, `root`, `current_branch`, `is_dirty`.
|
||||||
|
- Invariants : `root` contient (ou contiendra après init) un `.git`. État dérivé, rafraîchi via le port.
|
||||||
|
|
||||||
|
**`AgentManifest`** (entité — image en mémoire de `.ideai/agents.json`)
|
||||||
|
- Champs : `entries: Vec<ManifestEntry { agent_id, md_path, template_id?, synchronized, synced_template_version? }>`.
|
||||||
|
- Invariants : `synchronized ⇒ template_id.is_some() && synced_template_version.is_some()` ; `md_path` unique ; cohérence avec les `Agent` chargés.
|
||||||
|
|
||||||
|
**`Workspace` / `Window` / `Tab`** (entités de présentation persistée)
|
||||||
|
- `Workspace` = ensemble des fenêtres d'une session utilisateur.
|
||||||
|
- `Window` = fenêtre OS ; possède un `LayoutTree` **par onglet actif** et une liste de `Tab`.
|
||||||
|
- `Tab` = onglet ⇔ **un `Project`** (1:1).
|
||||||
|
- Invariants : un `Project` ouvert apparaît dans **exactement un** `Tab` à la fois (le drag déplace, ne duplique pas) ; un `Window` a ≥ 1 `Tab` ou est fermée.
|
||||||
|
|
||||||
|
**`DomainEvent`** (enum) — `ProjectCreated`, `AgentLaunched`, `AgentExited`, `TemplateUpdated`, `AgentDriftDetected`, `LayoutChanged`, `RemoteConnected`, `GitStateChanged`, `PtyOutput{session_id, bytes}` (ce dernier souvent court-circuité vers un Channel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ports (traits du domaine)
|
||||||
|
|
||||||
|
> Signatures **conceptuelles** (Rust idiomatique, `async` via `async_trait` ou retours `Future` ; erreurs typées par port). « Consommé par » = use cases. « Implémenté par » = adapters de §5.
|
||||||
|
|
||||||
|
### `AgentRuntime`
|
||||||
|
- **Rôle** : lancer/piloter la CLI d'une IA selon un `AgentProfile`, en gérant l'injection du contexte `.md`.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait AgentRuntime {
|
||||||
|
fn detect(&self, profile: &AgentProfile) -> Result<bool, RuntimeError>;
|
||||||
|
fn prepare_invocation(&self, profile: &AgentProfile, ctx: &PreparedContext, cwd: &ProjectPath)
|
||||||
|
-> Result<SpawnSpec, RuntimeError>; // commande + args + plan d'injection (fichier/flag/stdin/env)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `LaunchAgent`, `DetectProfilesUseCase` (first-run).
|
||||||
|
- **Implémenté par** : `CliAgentRuntime` (un seul adapter générique piloté par le profil déclaratif — c'est l'**Open/Closed**). La diversité des IA = données, pas code.
|
||||||
|
|
||||||
|
### `PtyPort` (alias domaine de `TerminalSessionPort`)
|
||||||
|
- **Rôle** : ouvrir un pseudo-terminal, lire/écrire, redimensionner, tuer.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait PtyPort {
|
||||||
|
async fn spawn(&self, spec: SpawnSpec, size: PtySize) -> Result<PtyHandle, PtyError>;
|
||||||
|
fn write(&self, h: &PtyHandle, data: &[u8]) -> Result<(), PtyError>;
|
||||||
|
fn resize(&self, h: &PtyHandle, size: PtySize) -> Result<(), PtyError>;
|
||||||
|
fn subscribe_output(&self, h: &PtyHandle) -> OutputStream; // flux d'octets
|
||||||
|
async fn kill(&self, h: &PtyHandle) -> Result<ExitStatus, PtyError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `OpenTerminal`, `LaunchAgent`, `CloseTerminal`.
|
||||||
|
- **Implémenté par** : `PortablePtyAdapter` (local), `SshPtyAdapter` (PTY distant via russh exec/shell), `WslPtyAdapter` (PTY via `wsl.exe`). Sélection par stratégie `RemoteRef` (Liskov).
|
||||||
|
|
||||||
|
### `RemoteHost`
|
||||||
|
- **Rôle** : abstraction de la **localisation d'exécution** (local / SSH / WSL) : exécuter une commande, ouvrir un PTY, accéder au FS, dans le bon contexte.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait RemoteHost {
|
||||||
|
fn kind(&self) -> RemoteKind;
|
||||||
|
async fn connect(&self) -> Result<(), RemoteError>;
|
||||||
|
fn file_system(&self) -> Arc<dyn FileSystem>;
|
||||||
|
fn process_spawner(&self) -> Arc<dyn ProcessSpawner>;
|
||||||
|
fn pty(&self) -> Arc<dyn PtyPort>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : tous les use cases qui touchent un projet (résolvent leurs ports via le `RemoteHost` du projet → **transparence local/distant**).
|
||||||
|
- **Implémenté par** : `LocalHost`, `SshHost` (russh/ssh2), `WslHost` (wsl.exe). C'est la **stratégie** qui unifie les 3 modes.
|
||||||
|
|
||||||
|
### `ProcessSpawner`
|
||||||
|
- **Rôle** : lancer un process **non interactif** et récupérer sortie/exit (ex. `detect`, commandes git hors libgit2, scripts).
|
||||||
|
- **Signature** : `async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError>;`
|
||||||
|
- **Consommé par** : `DetectProfilesUseCase`, services divers.
|
||||||
|
- **Implémenté par** : `LocalProcessSpawner`, `SshProcessSpawner`, `WslProcessSpawner`.
|
||||||
|
|
||||||
|
### `FileSystem`
|
||||||
|
- **Rôle** : lecture/écriture/listing/symlink, neutre vis-à-vis de la localisation.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait FileSystem {
|
||||||
|
async fn read(&self, p: &RemotePath) -> Result<Vec<u8>, FsError>;
|
||||||
|
async fn write(&self, p: &RemotePath, data: &[u8]) -> Result<(), FsError>;
|
||||||
|
async fn exists(&self, p: &RemotePath) -> Result<bool, FsError>;
|
||||||
|
async fn create_dir_all(&self, p: &RemotePath) -> Result<(), FsError>;
|
||||||
|
async fn list(&self, p: &RemotePath) -> Result<Vec<DirEntry>, FsError>;
|
||||||
|
async fn symlink(&self, src: &RemotePath, dst: &RemotePath) -> Result<(), FsError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `AgentContextStore`, `ProjectStore`, injection `conventionFile`, etc.
|
||||||
|
- **Implémenté par** : `LocalFileSystem` (std::fs/tokio::fs), `SshFileSystem` (SFTP), `WslFileSystem` (via `wsl.exe` ou chemins `\\wsl$`).
|
||||||
|
|
||||||
|
### `TemplateStore`
|
||||||
|
- **Rôle** : CRUD des `AgentTemplate` dans le store global IDE + versioning.
|
||||||
|
- **Signature** : `list / get / save / delete / bump_version`.
|
||||||
|
- **Consommé par** : `CreateTemplate`, `UpdateTemplate`, `CreateAgentFromTemplate`, `SyncAgentWithTemplate`.
|
||||||
|
- **Implémenté par** : `FsTemplateStore` (md + index json dans le dossier de données app).
|
||||||
|
|
||||||
|
### `ProjectStore`
|
||||||
|
- **Rôle** : persistance de la liste des projets connus, workspaces, windows, tabs, layouts.
|
||||||
|
- **Signature** : `list_projects / load_project / save_project / save_workspace / load_workspace`.
|
||||||
|
- **Consommé par** : `CreateProject`, `OpenProject`, persistance fenêtres/onglets/layout.
|
||||||
|
- **Implémenté par** : `FsProjectStore` (json dans données app pour le registre ; layout par projet dans `.ideai/`).
|
||||||
|
|
||||||
|
### `AgentContextStore`
|
||||||
|
- **Rôle** : lire/écrire les `.md` d'agents **et** le manifeste `.ideai/agents.json` (au sein du projet, via le `FileSystem` du `RemoteHost`).
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait AgentContextStore {
|
||||||
|
async fn read_context(&self, project: &Project, agent: &AgentId) -> Result<MarkdownDoc, StoreError>;
|
||||||
|
async fn write_context(&self, project: &Project, agent: &AgentId, md: &MarkdownDoc) -> Result<(), StoreError>;
|
||||||
|
async fn load_manifest(&self, project: &Project) -> Result<AgentManifest, StoreError>;
|
||||||
|
async fn save_manifest(&self, project: &Project, m: &AgentManifest) -> Result<(), StoreError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `CreateAgent*`, `LaunchAgent`, `SyncAgentWithTemplate`.
|
||||||
|
- **Implémenté par** : `IdeaiContextStore` (compose `FileSystem`, écrit `.ideai/`).
|
||||||
|
|
||||||
|
### `GitRepository`
|
||||||
|
- **Rôle** : opérations git du projet.
|
||||||
|
- **Signature** : `status / stage / unstage / commit / branches / checkout / current_branch / diff / log / pull / push / clone / init`.
|
||||||
|
- **Consommé par** : use cases Git.
|
||||||
|
- **Implémenté par** : `Git2Repository` (libgit2, local) ; sur SSH/WSL, `RemoteGitRepository` délègue à git CLI via `ProcessSpawner` quand libgit2 ne peut pas atteindre le FS distant (point ouvert §13).
|
||||||
|
|
||||||
|
### `EventBus`
|
||||||
|
- **Rôle** : publier/souscrire les `DomainEvent` (découple émetteurs et présentation).
|
||||||
|
- **Signature** : `fn publish(&self, e: DomainEvent); fn subscribe(&self) -> EventStream;`
|
||||||
|
- **Consommé par** : tous use cases (publient) ; l'adapter Tauri (souscrit → relaye en events/channels IPC).
|
||||||
|
- **Implémenté par** : `TokioBroadcastEventBus` (in-process), relayé par `TauriEventRelay`.
|
||||||
|
|
||||||
|
### `Clock` & `IdGenerator` (ports utilitaires — testabilité)
|
||||||
|
- **Rôle** : éliminer le non-déterminisme (`now()`, `uuid`) du domaine/application.
|
||||||
|
- **Implémenté par** : `SystemClock` / `UuidGenerator` (prod), `FixedClock` / `SeqIdGenerator` (tests).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Adapters (impl concrètes par port)
|
||||||
|
|
||||||
|
| Port | Adapter(s) | Techno | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `AgentRuntime` | `CliAgentRuntime` | piloté par `AgentProfile` | Construit `SpawnSpec` + plan d'injection. Un seul adapter, N profils. |
|
||||||
|
| `PtyPort` | `PortablePtyAdapter` | portable-pty | Local. Stream octets → Channel Tauri. |
|
||||||
|
| | `SshPtyAdapter` | russh (channel shell/exec + pty req) | Distant SSH. |
|
||||||
|
| | `WslPtyAdapter` | `wsl.exe -d <distro>` + portable-pty | PTY dans la distro. |
|
||||||
|
| `RemoteHost` | `LocalHost` / `SshHost` / `WslHost` | — / russh,ssh2 / wsl.exe | Stratégie ; fabrique FS/Spawner/PTY adaptés. |
|
||||||
|
| `ProcessSpawner` | `LocalProcessSpawner` | std/tokio `Command` | |
|
||||||
|
| | `SshProcessSpawner` | russh exec | |
|
||||||
|
| | `WslProcessSpawner` | `wsl.exe` | |
|
||||||
|
| `FileSystem` | `LocalFileSystem` | tokio::fs | |
|
||||||
|
| | `SshFileSystem` | SFTP (ssh2/russh-sftp) | |
|
||||||
|
| | `WslFileSystem` | `\\wsl$\` / `wsl.exe cat`… | |
|
||||||
|
| `TemplateStore` | `FsTemplateStore` | tokio::fs + serde_json | Dossier données app. |
|
||||||
|
| `ProjectStore` | `FsProjectStore` | tokio::fs + serde_json | Registre projets + workspace. |
|
||||||
|
| `AgentContextStore` | `IdeaiContextStore` | compose `FileSystem` | Écrit `.ideai/`. |
|
||||||
|
| `GitRepository` | `Git2Repository` | git2 | Local. |
|
||||||
|
| | `RemoteGitRepository` | git CLI via `ProcessSpawner` | SSH/WSL fallback. |
|
||||||
|
| `EventBus` | `TokioBroadcastEventBus` (+ `TauriEventRelay`) | tokio::broadcast | Relais vers IPC. |
|
||||||
|
| `Clock`/`IdGenerator` | `SystemClock`/`UuidGenerator` | std/uuid | Mocks en test. |
|
||||||
|
|
||||||
|
**Adapters entrants (driving)** : handlers `#[tauri::command]` (frontend → app) + `TauriEventRelay` (app → frontend). Côté UI : `tauri-adapters` implémentant les gateways TS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Use cases / services applicatifs
|
||||||
|
|
||||||
|
> Chaque use case : un struct `XxxUseCase` portant ses ports en `Arc<dyn Port>`, une méthode `execute(input: XxxInput) -> Result<XxxOutput, AppError>`. **Single Responsibility**. Aucune dépendance à Tauri.
|
||||||
|
|
||||||
|
| Use case | Rôle | Ports consommés |
|
||||||
|
|---|---|---|
|
||||||
|
| `CreateProject` | Crée un projet (project root), init `.ideai/`, registre. | `ProjectStore`, `FileSystem`, `IdGenerator`, `EventBus` |
|
||||||
|
| `OpenProject` | Charge projet, manifeste, layout, résout `RemoteHost`. | `ProjectStore`, `AgentContextStore`, `RemoteHost` |
|
||||||
|
| `CloseProject` / `CloseTab` | Persiste l'état, libère PTYs. | `ProjectStore`, `PtyPort`, `EventBus` |
|
||||||
|
| `DetectProfiles` (first-run) | Teste `detect` de chaque profil candidat. | `AgentRuntime`, `ProcessSpawner` |
|
||||||
|
| `ConfigureProfiles` | Enregistre profils choisis/édités/custom. | `TemplateStore`/profile store, `FileSystem` |
|
||||||
|
| `CreateAgentFromScratch` | Crée agent + `.md`, met à jour manifeste. | `AgentContextStore`, `IdGenerator` |
|
||||||
|
| `CreateAgentFromTemplate` | Copie le `content_md` du template → agent ; lie origine + version + `synchronized`. | `TemplateStore`, `AgentContextStore` |
|
||||||
|
| `UpdateTemplate` | Modifie un template, **bump version**, signale drift aux agents liés. | `TemplateStore`, `EventBus` |
|
||||||
|
| `DetectAgentDrift` | Compare `synced_template_version` vs `template.version`. | `TemplateStore`, `AgentContextStore` |
|
||||||
|
| `SyncAgentWithTemplate` | Applique la MAJ template→agent si `synchronized`. | `TemplateStore`, `AgentContextStore`, `EventBus` |
|
||||||
|
| `LaunchAgent` | Résout profil+contexte, prépare injection, ouvre cellule PTY au bon `cwd`, spawn CLI. | `AgentRuntime`, `AgentContextStore`, `RemoteHost`→`PtyPort`/`FileSystem`, `EventBus` |
|
||||||
|
| `OpenTerminal` | Ouvre un PTY simple dans une cellule. | `RemoteHost`→`PtyPort`, `EventBus` |
|
||||||
|
| `WriteToTerminal` / `ResizeTerminal` / `CloseTerminal` | I/O PTY. | `PtyPort` |
|
||||||
|
| `MutateLayout` (split/merge/resize/move) | Applique une opération sur le `LayoutTree` (logique **pure** dans le domaine, persistée ici). | `ProjectStore` (persistance) |
|
||||||
|
| `ConnectRemote` (SSH/WSL) | Établit la connexion, valide l'accès au root. | `RemoteHost`, `FileSystem` |
|
||||||
|
| `MoveTabToNewWindow` | Détache un onglet → nouvelle fenêtre (réaffectation `WindowId`). | `ProjectStore`, `EventBus` |
|
||||||
|
| Use cases Git | `GitStatus`, `GitCommit`, `GitCheckout`, `GitPush`, … | `GitRepository`, `EventBus` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Modèle de layout terminal (grille tableur récursive + fusion)
|
||||||
|
|
||||||
|
### 7.1 Structure de données
|
||||||
|
|
||||||
|
La grille « type tableur, lignes/colonnes imbriquées indépendamment + fusion » est modélisée par un **arbre de splits récursif** où chaque conteneur définit son propre découpage. La **fusion** est obtenue nativement : fusionner = ne pas subdiviser une zone (un `Leaf` couvre plusieurs « cellules visuelles » d'un parent voisin). Pour le cas Excel pur (fusion arbitraire chevauchant la grille), on superpose un modèle **GridContainer** avec spans.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum LayoutNode {
|
||||||
|
Leaf(LeafCell),
|
||||||
|
Split(SplitContainer),
|
||||||
|
Grid(GridContainer),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LeafCell {
|
||||||
|
id: NodeId,
|
||||||
|
session: Option<SessionId>, // 0 ou 1 terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SplitContainer { // découpage simple binaire/n-aire pondéré
|
||||||
|
id: NodeId,
|
||||||
|
direction: Direction, // Row (colonnes) | Column (lignes)
|
||||||
|
children: Vec<WeightedChild>, // ordre = gauche→droite / haut→bas
|
||||||
|
}
|
||||||
|
struct WeightedChild { node: LayoutNode, weight: f32 } // poids = part redimensionnable
|
||||||
|
|
||||||
|
struct GridContainer { // grille tableur avec fusion (spans)
|
||||||
|
id: NodeId,
|
||||||
|
col_weights: Vec<f32>, // largeurs de colonnes
|
||||||
|
row_weights: Vec<f32>, // hauteurs de lignes
|
||||||
|
cells: Vec<GridCell>, // placements avec spans (fusion)
|
||||||
|
}
|
||||||
|
struct GridCell {
|
||||||
|
node: LayoutNode, // récursif : une cellule peut re-contenir un Split/Grid
|
||||||
|
row: u16, col: u16,
|
||||||
|
row_span: u16, // ≥1 ; >1 = cellules fusionnées verticalement
|
||||||
|
col_span: u16, // ≥1 ; >1 = cellules fusionnées horizontalement
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Lignes/colonnes indépendantes par zone** : chaque `SplitContainer`/`GridContainer` a ses propres poids ⇒ pas de grille uniforme rigide.
|
||||||
|
- **Imbrication** : un enfant peut être un nouveau `Split`/`Grid` ⇒ « N colonnes dans une ligne, M lignes dans une colonne » de façon arbitraire.
|
||||||
|
- **Fusion** : `row_span`/`col_span` dans `GridContainer` (modèle tableur fidèle) **ou** simplement un `Leaf` plus grand via `SplitContainer` (cas courant). Le domaine supporte les deux ; l'UI choisit la représentation selon l'interaction.
|
||||||
|
|
||||||
|
### 7.2 Invariants (validés dans le domaine, testables sans I/O)
|
||||||
|
|
||||||
|
- Tous les `weight > 0`. Les poids sont **relatifs** (l'UI normalise pour le rendu).
|
||||||
|
- Dans un `GridContainer` : aucune superposition de spans ; toute la surface couverte ; `row+row_span ≤ rows`, `col+col_span ≤ cols`.
|
||||||
|
- Un `SessionId` n'apparaît que dans **un seul** `Leaf`.
|
||||||
|
- Les opérations `split`, `merge`, `resize`, `move` sont des **fonctions pures** `LayoutTree -> Result<LayoutTree, LayoutError>` (immutabilité ⇒ testabilité, undo/redo facile).
|
||||||
|
|
||||||
|
### 7.3 Sérialisation & persistance
|
||||||
|
|
||||||
|
- Sérialisé en **JSON** (serde, `tag`/`content` pour l'enum) → `.ideai/layout.json` (par projet, donc voyage avec le projet, y compris distant).
|
||||||
|
- Le `Workspace`/`Window`/`Tab` (organisation des fenêtres OS) est persisté côté **store global IDE** (machine-local, pas dans le projet) car lié à l'écran de l'utilisateur, pas au code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Synchronisation template → agents
|
||||||
|
|
||||||
|
### 8.1 Versioning
|
||||||
|
|
||||||
|
- `AgentTemplate.version: u64` monotone. **`UpdateTemplate` incrémente** la version à chaque changement de `content_md`. Un hash du contenu (`content_hash`) est aussi stocké pour détecter les éditions hors-app.
|
||||||
|
- Chaque `ManifestEntry` d'agent lié garde `synced_template_version` = version du template **au dernier sync réussi**.
|
||||||
|
|
||||||
|
### 8.2 Détection de drift
|
||||||
|
|
||||||
|
```
|
||||||
|
drift(agent) =
|
||||||
|
agent.synchronized
|
||||||
|
&& agent.origin == FromTemplate{ template_id, .. }
|
||||||
|
&& template_store.get(template_id).version > entry.synced_template_version
|
||||||
|
```
|
||||||
|
`DetectAgentDrift` est lancé à `OpenProject` et après chaque `UpdateTemplate` ; émet `AgentDriftDetected { agent_id, from, to }` → badge UI.
|
||||||
|
|
||||||
|
### 8.3 Application de la MAJ (`SyncAgentWithTemplate`)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Charger template (version courante) + manifeste projet.
|
||||||
|
2. Pour chaque agent ciblé avec synchronized==true :
|
||||||
|
a. Stratégie de MAJ = REMPLACEMENT du .md par content_md du template
|
||||||
|
(le contexte d'un agent synchronisé est "possédé" par le template).
|
||||||
|
→ Variante future : merge 3-way si l'agent a un bloc local marqué.
|
||||||
|
b. write_context(agent, template.content_md)
|
||||||
|
c. entry.synced_template_version = template.version
|
||||||
|
3. save_manifest. publish(AgentSynced{..}).
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Agents non synchronisés
|
||||||
|
|
||||||
|
- `synchronized == false` : ne reçoivent **jamais** de MAJ auto. Ils gardent leur `.md` libre. On peut afficher « une nouvelle version du template existe » (info) mais aucune écriture n'a lieu sans action explicite (qui basculerait `synchronized` ou ferait un sync ponctuel one-shot).
|
||||||
|
- Agents `Scratch` : aucun lien template, hors périmètre de sync.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Stockage & arborescence des fichiers
|
||||||
|
|
||||||
|
### 9.1 Dans le projet — `.ideai/` (voyage avec le code, versionnable)
|
||||||
|
|
||||||
|
```
|
||||||
|
<project_root>/
|
||||||
|
├── .ideai/
|
||||||
|
│ ├── agents.json # AgentManifest (mapping md ↔ template ↔ sync ↔ version)
|
||||||
|
│ ├── layout.json # LayoutTree de l'onglet (sérialisé)
|
||||||
|
│ ├── project.json # méta projet local (nom, profil par défaut, remote ref)
|
||||||
|
│ └── agents/
|
||||||
|
│ ├── reviewer.md # contexte d'un agent de projet
|
||||||
|
│ ├── backend-dev.md
|
||||||
|
│ └── ...
|
||||||
|
└── (CLAUDE.md / AGENTS.md / GEMINI.md générés/symlinkés à l'activation si conventionFile)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schéma `agents.json`** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"id": "a3f1...",
|
||||||
|
"name": "Backend Dev",
|
||||||
|
"md": "agents/backend-dev.md",
|
||||||
|
"profileId": "claude-code",
|
||||||
|
"origin": { "type": "fromTemplate", "templateId": "tpl-backend", "syncedTemplateVersion": 4 },
|
||||||
|
"synchronized": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b7c2...",
|
||||||
|
"name": "Ad-hoc",
|
||||||
|
"md": "agents/adhoc.md",
|
||||||
|
"profileId": "codex-cli",
|
||||||
|
"origin": { "type": "scratch" },
|
||||||
|
"synchronized": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Store global IDE (données app, hors projet, machine-local)
|
||||||
|
|
||||||
|
Emplacement résolu via Tauri path API (`AppData`/`~/.local/share/IdeA`/`~/Library/Application Support/IdeA`).
|
||||||
|
|
||||||
|
```
|
||||||
|
<app_data_dir>/IdeA/
|
||||||
|
├── profiles.json # AgentProfile[] configurés (first-run + custom + édités)
|
||||||
|
├── settings.json # préférences IDE
|
||||||
|
├── workspace.json # Workspace/Window/Tab + quel projet dans quel onglet (machine-local)
|
||||||
|
└── templates/
|
||||||
|
├── index.json # [{id, name, version, contentHash, defaultProfileId}]
|
||||||
|
└── md/
|
||||||
|
├── tpl-backend.md
|
||||||
|
├── tpl-reviewer.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schéma `profiles.json` (item)** : exactement le profil déclaratif de CONTEXT.md §9 (`id, name, command, args, contextInjection{strategy,target/flag/var}, detect, cwd`).
|
||||||
|
|
||||||
|
**Formats** : contextes & templates en **Markdown** ; tout le reste en **JSON** (serde). Pas de base de données : fichiers plats, simples, diffables, portables (AppImage friendly).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Arborescence du repo
|
||||||
|
|
||||||
|
### 10.1 Décision : workspace Cargo **multi-crate**
|
||||||
|
|
||||||
|
**Multi-crate** retenu (vs mono-crate) pour **forcer** la règle de dépendance à la compilation : le crate `domain` ne peut littéralement pas dépendre de `infrastructure` si ce n'est pas dans son `Cargo.toml`. C'est la garantie mécanique de l'hexagonal (mieux qu'une convention). Coût : un peu de cérémonie de workspace — acceptable et même souhaitable ici vu le découpage en lots/agents (§12).
|
||||||
|
|
||||||
|
```
|
||||||
|
IdeA/
|
||||||
|
├── Cargo.toml # [workspace] members
|
||||||
|
├── ARCHITECTURE.md
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── crates/
|
||||||
|
│ ├── domain/ # PUR : entities, VO, ports (traits), domain events, layout logic
|
||||||
|
│ │ └── src/{project,agent,template,profile,terminal,layout,remote,git,ports,events}.rs
|
||||||
|
│ ├── application/ # use cases, DTOs, AppError ; dépend de domain
|
||||||
|
│ │ └── src/{project,agent,template,terminal,layout,remote,git}/
|
||||||
|
│ ├── infrastructure/ # adapters ; dépend de domain (+ application pour DTO si besoin)
|
||||||
|
│ │ └── src/{pty,fs,process,remote,git,store,runtime,eventbus}/
|
||||||
|
│ └── app-tauri/ # binaire Tauri : commands, events, COMPOSITION ROOT (DI)
|
||||||
|
│ ├── src/{commands,events,state,main.rs}
|
||||||
|
│ ├── tauri.conf.json
|
||||||
|
│ ├── build.rs
|
||||||
|
│ └── icons/, bundle (NSIS + AppImage)
|
||||||
|
├── frontend/ # TypeScript + React (Vite)
|
||||||
|
│ ├── package.json, vite.config.ts, index.html
|
||||||
|
│ └── src/
|
||||||
|
│ ├── domain/ # types & logique de vue purs (miroir DTO, calc layout)
|
||||||
|
│ ├── ports/ # gateways TS (interfaces) : AgentGateway, TerminalGateway, ...
|
||||||
|
│ ├── adapters/ # impl gateways via @tauri-apps/api (invoke/listen/Channel)
|
||||||
|
│ │ └── mock/ # impl mock pour dev/test/storybook
|
||||||
|
│ ├── features/ # par feature : projects, agents, templates, terminals, layout, git, remote, first-run
|
||||||
|
│ │ └── <feature>/{components,hooks,store,index.ts}
|
||||||
|
│ ├── shared/ # ui kit, xterm wrapper, design system
|
||||||
|
│ └── app/ # bootstrap, routing, providers (DI des adapters)
|
||||||
|
└── docs/ # ADRs, schémas
|
||||||
|
```
|
||||||
|
|
||||||
|
`app-tauri` = **seul** endroit qui connaît tous les crates : il instancie les adapters concrets et injecte dans les use cases (composition root). Personne d'autre ne fait de `new ConcreteAdapter`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Stratégie de tests
|
||||||
|
|
||||||
|
| Couche | Type de test | Comment / où |
|
||||||
|
|---|---|---|
|
||||||
|
| `domain` | **Unitaires purs** (sans I/O, sans async) | `#[cfg(test)] mod tests` par module. Invariants d'entités, opérations de layout (split/merge/resize), détection de drift, validation `ContextInjection`. Déterministe via `FixedClock`/`SeqIdGenerator`. |
|
||||||
|
| `application` | **Unitaires avec ports mockés** | Chaque use case testé avec des **mocks de ports** (`mockall` ou fakes manuels). Ex. `LaunchAgent` vérifie qu'il appelle `prepare_invocation` puis `pty.spawn` avec le bon `cwd` et plan d'injection. **Aucun vrai PTY/FS/git.** |
|
||||||
|
| `infrastructure` | **Tests d'intégration ciblés** | Par adapter : `LocalFileSystem` sur tmpdir, `Git2Repository` sur repo temporaire, `PortablePtyAdapter` lance `echo`. SSH/WSL : tests `#[ignore]` gated derrière feature/env (CI conditionnelle). |
|
||||||
|
| `app-tauri` | Tests des commands (mapping DTO ↔ use case) | Wiring testé avec use cases réels + adapters in-memory. |
|
||||||
|
| Frontend `domain`/`ports` | **Vitest** (unitaires purs) | Logique de vue, calc tailles cellules, réducteurs de state. |
|
||||||
|
| Frontend `features` | **React Testing Library** + **gateways mock** | Composants testés avec adapters mock ⇒ **sans backend**. |
|
||||||
|
| E2E (plus tard) | Playwright / `tauri-driver` | Smoke tests des parcours clés. |
|
||||||
|
|
||||||
|
**Clé de testabilité** : grâce aux **ports**, le domaine et l'application se testent **100 % sans I/O**. C'est l'argument central de l'hexagonal et le socle du cycle dev↔test (chaque agent dev appairé à un agent test, cf. CONTEXT §3). Règle d'or : une feature n'est verte que quand `cargo test -p <crate>` et `vitest` passent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Découpage en lots/features livrables
|
||||||
|
|
||||||
|
> Chaque lot = périmètre autonome, validable par le cycle dev/test, confiable à **un binôme (agent dev + agent test)**. Ordonnés par dépendance.
|
||||||
|
|
||||||
|
| # | Lot | Contenu | Crates/zones |
|
||||||
|
|---|---|---|---|
|
||||||
|
| L0 | **Socle domaine & ports** | Entities, VO, **tous les traits ports**, domain events, `AppError`. Aucun adapter. | `domain` (+ ports utilitaires) |
|
||||||
|
| L1 | **Composition root & IPC** | `app-tauri` : DI, registre de commands/events, bridge PTY↔Channel, gateways TS + adapters Tauri + mocks. | `app-tauri`, `frontend/ports`+`adapters` |
|
||||||
|
| L2 | **Projets & stockage** | `CreateProject`/`OpenProject`/`CloseProject`, `FsProjectStore`, `LocalFileSystem`, init `.ideai/`. UI projets/onglets. | `application/project`, `infrastructure/{fs,store}`, `frontend/features/projects` |
|
||||||
|
| L3 | **Terminaux & PTY (local)** | `PtyPort` + `PortablePtyAdapter`, use cases terminal, wrapper xterm.js, flux Channel. | `infrastructure/pty`, `application/terminal`, `frontend/features/terminals` |
|
||||||
|
| L4 | **Layout tableur** | Logique pure `LayoutTree` (déjà en L0 partiellement), `MutateLayout`, persistance `layout.json`, UI grille redimensionnable + fusion. | `domain/layout`, `application/layout`, `frontend/features/layout` |
|
||||||
|
| L5 | **Profils IA & runtime** | `AgentProfile`, `CliAgentRuntime`, `DetectProfiles`, first-run wizard, `profiles.json`. | `infrastructure/runtime`, `application/agent`, `frontend/features/first-run` |
|
||||||
|
| L6 | **Agents & contextes** | `AgentContextStore`/`IdeaiContextStore`, CRUD agents, `LaunchAgent` (injection + spawn + cellule). | `application/agent`, `infrastructure/store`, `frontend/features/agents` |
|
||||||
|
| L7 | **Templates & synchro** | `TemplateStore`, versioning, `DetectAgentDrift`, `SyncAgentWithTemplate`. UI templates + badges drift. | `application/template`, `infrastructure/store`, `frontend/features/templates` |
|
||||||
|
| L8 | **Git** | `GitRepository`/`Git2Repository`, use cases git, UI git. | `infrastructure/git`, `application/git`, `frontend/features/git` |
|
||||||
|
| L9 | **Remote (SSH + WSL)** | `RemoteHost` stratégie, `SshHost`/`WslHost`, adapters FS/PTY/Spawner distants, `RemoteGitRepository`. UI connexion. | `infrastructure/remote`, `application/remote`, `frontend/features/remote` |
|
||||||
|
| L10 | **Fenêtres & multi-window** | `Workspace`/`Window`/`Tab`, `MoveTabToNewWindow`, drag d'onglet → nouvelle fenêtre OS Tauri. | `application`, `app-tauri`, `frontend/app` |
|
||||||
|
| L11 | **Packaging & livraison** | Tauri bundle : NSIS `setup.exe`, **AppImage** multi-distro, CI Linux+Windows. | `app-tauri`, CI |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Risques techniques & points ouverts (spikes)
|
||||||
|
|
||||||
|
1. **PTY cross-platform** : portable-pty + xterm.js OK sur les 3 OS, mais signaux/resize/exit codes diffèrent (Windows ConPTY). **Spike** L3.
|
||||||
|
2. **AppImage multi-distro** : libgit2/openssl/glibc liés dynamiquement → risque de non-portabilité. **Spike** : vendoring statique (`git2` features, `rustls` pour russh au lieu d'OpenSSL), test sur ≥3 distros (Ubuntu/Fedora/Arch). L11.
|
||||||
|
3. **Drag d'onglet entre fenêtres Tauri** : Tauri v2 multi-webview/multi-window + DnD natif inter-fenêtres est délicat (le DnD HTML ne traverse pas les fenêtres OS). **Spike** : protocole « detach » (créer une `WebviewWindow`, transférer l'état via store + event, fermer l'onglet source). L10.
|
||||||
|
4. **Git sur FS distant** : libgit2 ne lit pas un FS SSH/WSL directement. Décision : **fallback git CLI** (`RemoteGitRepository`) côté distant via `ProcessSpawner`. À valider (perf, parsing). L9.
|
||||||
|
5. **Synchro temps réel UI ↔ PTY** : volume d'octets élevé ; backpressure des Channels Tauri, throttling/coalescing côté front. **Spike** L3.
|
||||||
|
6. **Injection `conventionFile`** : symlink vs copie du `.md` vers `CLAUDE.md`/`AGENTS.md` ; conflits si fichier existant, .gitignore, droits Windows (symlinks). À cadrer L6.
|
||||||
|
7. **SSH auth** : agent/clé/mot de passe/known_hosts ; choix russh (rustls) vs ssh2 (libssh2/OpenSSL — impacte point 2). Décision à figer début L9.
|
||||||
|
8. **WSL chemins** : conversion `/mnt/c/...` ↔ `\\wsl$\...`, distros multiples, perf I/O cross-boundary. Spike L9.
|
||||||
|
9. **Détection d'édition hors-app** des `.md`/templates (content hash) et résolution de conflit lors du sync. L7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document maintenu par l'Agent Architecture — base du jalon « cadrage architecture » avant tout code applicatif.*
|
||||||
187
.ideai/agents/main.md
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# IdeA — Contexte & Méthode de travail
|
||||||
|
|
||||||
|
> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA.
|
||||||
|
> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Mon rôle : chef d'orchestre, pas développeur
|
||||||
|
|
||||||
|
Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail.
|
||||||
|
Je suis responsable de :
|
||||||
|
|
||||||
|
- Découper le travail en tâches claires et autonomes.
|
||||||
|
- Attribuer chaque tâche aux bons agents.
|
||||||
|
- Garantir que le cycle de développement/test est respecté.
|
||||||
|
- Faire respecter les principes d'architecture (SOLID, Hexagonal).
|
||||||
|
- Maintenir la cohérence globale du projet et de ce document.
|
||||||
|
- Arbitrer et valider avant toute action irréversible ou sortante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Les agents
|
||||||
|
|
||||||
|
### 2.1 Agent Architecture (1 pour tout le projet)
|
||||||
|
- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**.
|
||||||
|
- Définit les frontières (domaine / application / infrastructure), les ports, les contrats.
|
||||||
|
- Valide que chaque nouvelle feature respecte la structure avant son développement.
|
||||||
|
- Tient à jour la cartographie d'architecture et les conventions.
|
||||||
|
|
||||||
|
### 2.2 Agents de Développement
|
||||||
|
- Écrivent le code des features.
|
||||||
|
- Respectent strictement l'architecture définie par l'agent Architecture.
|
||||||
|
- Code **propre, structuré, stable**.
|
||||||
|
- Reçoivent les rapports d'erreurs des agents de test et corrigent.
|
||||||
|
|
||||||
|
### 2.3 Agents de Test
|
||||||
|
- **Chaque agent de développement est appairé avec un agent de test dédié.**
|
||||||
|
- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées.
|
||||||
|
- Produisent un **rapport d'erreurs** clair quand un test échoue.
|
||||||
|
- Re-testent après chaque correction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Le cycle de développement (boucle obligatoire)
|
||||||
|
|
||||||
|
Pour **chaque** feature implémentée ou modifiée :
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Agent Architecture → valide le découpage et les contrats (ports/interfaces)
|
||||||
|
2. Agent Développement → écrit le code
|
||||||
|
3. Agent Test → écrit les tests unitaires + les exécute
|
||||||
|
4a. Tests OK → feature validée, on passe à la suite
|
||||||
|
4b. Tests KO → rapport d'erreurs → retour à l'agent Développement
|
||||||
|
→ correction → retour à l'étape 3 (boucle jusqu'au vert)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas.
|
||||||
|
Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Principes de code
|
||||||
|
|
||||||
|
- **SOLID** appliqué au maximum.
|
||||||
|
- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...).
|
||||||
|
- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe.
|
||||||
|
- Tests unitaires systématiques ; couverture des features critiques.
|
||||||
|
- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Vision produit : IdeA
|
||||||
|
|
||||||
|
**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.**
|
||||||
|
|
||||||
|
### Fonctionnalités clés
|
||||||
|
- **Multi-projets en parallèle** : un **onglet par projet**.
|
||||||
|
- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement.
|
||||||
|
- **Agents par projet** : chaque projet a ses propres agents.
|
||||||
|
- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets.
|
||||||
|
- **Création d'agents** : depuis zéro ou à partir d'un template.
|
||||||
|
- **Synchronisation template → agents** : option « garder l'agent à jour ».
|
||||||
|
Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour.
|
||||||
|
- **Contextes d'agents stockés en `.md`** (toujours).
|
||||||
|
- **Création de projet** = définition de son **project root**.
|
||||||
|
|
||||||
|
### Intégrations
|
||||||
|
- **Git** intégré.
|
||||||
|
- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH.
|
||||||
|
- **Développement WSL** : travailler sur une WSL depuis Windows.
|
||||||
|
|
||||||
|
### Plateformes & livraison
|
||||||
|
- Cible : **macOS, Linux, Windows**.
|
||||||
|
- Première phase de compilation : **Linux et Windows**.
|
||||||
|
- Livraison :
|
||||||
|
- **Windows** : `setup.exe`.
|
||||||
|
- **Linux** : **AppImage** (doit fonctionner sur les différentes distributions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Stack technique (validée)
|
||||||
|
|
||||||
|
- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs).
|
||||||
|
- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations).
|
||||||
|
- **Frontend / UI** : **TypeScript + React**.
|
||||||
|
- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust).
|
||||||
|
- **Git** : **libgit2** via `git2` (Rust).
|
||||||
|
- **SSH** : `russh` / `ssh2` (Rust).
|
||||||
|
- **WSL** : invocation de `wsl.exe` depuis le backend.
|
||||||
|
|
||||||
|
## 7. Layout des terminaux (exigence produit)
|
||||||
|
|
||||||
|
Disposition en **grille redimensionnable de type tableur (Excel)** :
|
||||||
|
|
||||||
|
- Splits redimensionnables horizontaux **et** verticaux.
|
||||||
|
- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone.
|
||||||
|
- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur.
|
||||||
|
- Chaque cellule de la grille héberge un terminal.
|
||||||
|
- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture.
|
||||||
|
|
||||||
|
## 8. Stockage des contextes & liaison aux templates
|
||||||
|
|
||||||
|
- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet).
|
||||||
|
- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root.
|
||||||
|
*(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)*
|
||||||
|
- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet :
|
||||||
|
- le `.md` de l'agent,
|
||||||
|
- le template d'origine (le cas échéant),
|
||||||
|
- `synchronized: true/false`,
|
||||||
|
- la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible).
|
||||||
|
- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ.
|
||||||
|
|
||||||
|
## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`)
|
||||||
|
|
||||||
|
Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA :
|
||||||
|
|
||||||
|
1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`).
|
||||||
|
2. **Stratégie d'injection du contexte `.md`** :
|
||||||
|
- `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…).
|
||||||
|
- `flag` : passer le chemin via un argument.
|
||||||
|
- `stdin` : piper le contenu.
|
||||||
|
- `env` : passer via variable d'environnement.
|
||||||
|
|
||||||
|
Exemple de profil :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "claude-code",
|
||||||
|
"name": "Claude Code",
|
||||||
|
"command": "claude",
|
||||||
|
"args": [],
|
||||||
|
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||||
|
"detect": "claude --version",
|
||||||
|
"cwd": "{projectRoot}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message).
|
||||||
|
|
||||||
|
**Règles produit :**
|
||||||
|
- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut.
|
||||||
|
- Les commandes des profils sont **pré-remplies mais éditables**.
|
||||||
|
- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA.
|
||||||
|
|
||||||
|
**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil.
|
||||||
|
|
||||||
|
## 10. Fenêtres & onglets
|
||||||
|
|
||||||
|
- **Par défaut : un onglet par projet** (comme les IDE classiques).
|
||||||
|
- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet.
|
||||||
|
- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets.
|
||||||
|
|
||||||
|
## 11. Feuille de route
|
||||||
|
|
||||||
|
1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**.
|
||||||
|
2. Puis MVP incrémental selon le cycle dev/test de la section 3.
|
||||||
|
|
||||||
|
## 12. Autonomie d'exécution dans le projet
|
||||||
|
|
||||||
|
L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**.
|
||||||
|
|
||||||
|
- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**.
|
||||||
|
- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…).
|
||||||
|
- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 2026-06-05*
|
||||||
56
.ideai/layouts.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"activeId": "9188db80-8535-4786-a20b-3c9a36b222e3",
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"id": "9188db80-8535-4786-a20b-3c9a36b222e3",
|
||||||
|
"name": "Default",
|
||||||
|
"kind": "terminal",
|
||||||
|
"tree": {
|
||||||
|
"root": {
|
||||||
|
"type": "split",
|
||||||
|
"node": {
|
||||||
|
"id": "8aca2f93-1a9b-4693-9bba-9a01e130a48c",
|
||||||
|
"direction": "row",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"type": "leaf",
|
||||||
|
"node": {
|
||||||
|
"id": "d8a86eb1-cd4d-4937-b900-4989da7c868d",
|
||||||
|
"session": "2e6293fe-250a-4654-a8b2-4fe6996f8288",
|
||||||
|
"agent": "a6ced819-b893-4213-b003-9e9dc79b9641"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weight": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"type": "leaf",
|
||||||
|
"node": {
|
||||||
|
"id": "6c5be5e7-a54b-468c-a2e2-8ec853629d5e",
|
||||||
|
"session": "9aa5859c-5868-46b3-a045-68cbea6eb629"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weight": 1.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "40ea4fa9-25b3-410b-9d3e-6350834b421b",
|
||||||
|
"name": "Git Graph",
|
||||||
|
"kind": "gitGraph",
|
||||||
|
"tree": {
|
||||||
|
"root": {
|
||||||
|
"type": "leaf",
|
||||||
|
"node": {
|
||||||
|
"id": "c840bfdd-3330-46a3-b727-0799f6853e72"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
.ideai/project.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"id": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023",
|
||||||
|
"name": "IdeA",
|
||||||
|
"remote": {
|
||||||
|
"kind": "local"
|
||||||
|
},
|
||||||
|
"createdAt": 1780702317785
|
||||||
|
}
|
||||||
685
ARCHITECTURE.md
Normal file
@ -0,0 +1,685 @@
|
|||||||
|
# IdeA — Cartographie d'Architecture
|
||||||
|
|
||||||
|
> Document de référence produit par l'**Agent Architecture**.
|
||||||
|
> Fait autorité sur les frontières, ports, adapters, modules et conventions.
|
||||||
|
> Toute feature DOIT être validée contre ce document avant développement.
|
||||||
|
> Architecture **Hexagonale (Ports & Adapters)** + **SOLID**, stricte.
|
||||||
|
>
|
||||||
|
> Stack non négociable : Tauri v2 (shell) · Rust (cœur hexagonal) · TypeScript + React (UI) · xterm.js + portable-pty (terminaux) · git2/libgit2 · russh/ssh2 · wsl.exe.
|
||||||
|
>
|
||||||
|
> **Principe fondateur** : IdeA est un IDE 100 % IA dont le rôle est de **refléter fidèlement la façon dont on travaille avec des IAs** — sans jamais dépendre des commandes, flags ou conventions d'un modèle en particulier. Les deux abstractions de premier rang sont les **Agents** (instances IA à rôle/contexte définis) et les **Skills** (workflows réutilisables). Ces deux concepts sont gérés par IdeA de façon universelle : un utilisateur qui passe de Claude Code à Gemini CLI ou Codex retrouve exactement les mêmes Agents et Skills — seul le moteur d'exécution change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Principes : SOLID + Hexagonal, appliqués concrètement
|
||||||
|
|
||||||
|
### 1.1 Règle de dépendance (la seule qui compte)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Le sens des dépendances │
|
||||||
|
│ │
|
||||||
|
Présentation ─► Application ─► Domaine ◄─ Infrastructure │
|
||||||
|
(React/Tauri) (use cases) (pur) (adapters) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Le Domaine ne dépend de RIEN** : ni Tauri, ni tokio, ni git2, ni portable-pty, ni serde (le moins possible — voir §1.4). Il ne contient que des entités, value objects, règles métier et **traits = ports**.
|
||||||
|
- **L'Application** dépend du Domaine. Elle orchestre les use cases en parlant **uniquement aux ports** (traits), jamais aux adapters concrets.
|
||||||
|
- **L'Infrastructure** dépend du Domaine et de l'Application (elle implémente les ports). Elle contient tous les détails techniques (PTY, FS, git, SSH, WSL, stores).
|
||||||
|
- **La Présentation** (Tauri commands + React) dépend de l'Application. Les commandes Tauri sont des **adapters entrants (driving adapters)** ; les impl de ports sont des **adapters sortants (driven adapters)**.
|
||||||
|
|
||||||
|
Aucune flèche ne pointe **vers** la présentation ou l'infrastructure. L'inversion de dépendance (le **D** de SOLID) est matérialisée par les traits définis dans le domaine et implémentés dehors.
|
||||||
|
|
||||||
|
### 1.2 SOLID, point par point, traduit IdeA
|
||||||
|
|
||||||
|
| Principe | Application concrète |
|
||||||
|
|---|---|
|
||||||
|
| **S** — Single Responsibility | Un use case = une intention métier (`LaunchAgent`, `SyncAgentWithTemplate`). Un adapter = une techno (`Git2Repository` ne fait que du git). Le `LayoutNode` ne gère que la topologie, pas le rendu. |
|
||||||
|
| **O** — Open/Closed | Ajouter une IA = ajouter un **profil déclaratif** (donnée), pas du code. Ajouter un mode distant = nouvel adapter `RemoteHost` sans toucher aux use cases. Ajouter une stratégie d'injection de contexte = nouvelle variante d'enum + handler, use case inchangé. |
|
||||||
|
| **L** — Liskov | Tout `RemoteHost` (local, SSH, WSL) est substituable : un use case marche identiquement quelle que soit l'impl. Les contrats (pré/postconditions) des ports sont documentés et respectés par chaque adapter. |
|
||||||
|
| **I** — Interface Segregation | Ports **fins et ciblés** : `ProcessSpawner`, `FileSystem`, `PtyPort` séparés plutôt qu'un `System` fourre-tout. Un use case ne reçoit que les ports qu'il consomme. |
|
||||||
|
| **D** — Dependency Inversion | Domaine définit les traits ; infra les implémente ; l'application reçoit des `Arc<dyn Port>` par **injection** (composition root dans la couche Tauri). |
|
||||||
|
|
||||||
|
### 1.3 Hexagonal côté Frontend (React aussi)
|
||||||
|
|
||||||
|
L'hexagonal ne s'arrête pas à Rust. Côté React on applique le même découpage :
|
||||||
|
|
||||||
|
- **Domaine UI / modèles de vue** : types TS purs (miroir des DTO), logique de présentation pure (ex. calcul de tailles de cellules d'un `LayoutNode`), testable sans React ni Tauri.
|
||||||
|
- **Ports UI** : interfaces TS (`AgentGateway`, `TerminalGateway`, `ProjectGateway`, `LayoutGateway`, `GitGateway`, `RemoteGateway`) décrivant **ce dont l'UI a besoin**, indépendamment du transport.
|
||||||
|
- **Adapters UI** : implémentation des ports via `@tauri-apps/api` (`invoke` pour commands, `listen` pour events). Remplaçables par des **mocks** en test/Storybook.
|
||||||
|
- **Présentation** : composants React, hooks, state (Zustand/Redux) qui consomment les ports UI, jamais `invoke()` en direct.
|
||||||
|
|
||||||
|
Bénéfice : le frontend est testable et développable sans backend (adapters mock), et la frontière IPC est centralisée en un seul endroit.
|
||||||
|
|
||||||
|
### 1.4 Domaine pur vs adapters — règle pratique Rust
|
||||||
|
|
||||||
|
- Le crate `domain` est **`#![no_std]`-friendly d'esprit** (pas imposé), sans dépendance I/O. Tolérance pragmatique : `serde` est autorisé **uniquement** pour dériver la (dé)sérialisation des entités persistées (manifeste, layout, profils), car c'est une contrainte métier de format, pas un détail technique d'I/O. Les **traits/ports** y vivent. Pas de `tokio`, pas de `std::process`, pas de `std::fs`.
|
||||||
|
- Tout ce qui touche le monde réel (`std::fs`, `Command`, sockets, libgit2, PTY) vit **exclusivement** dans `infrastructure`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Découpage en couches & frontière Rust ↔ Tauri ↔ React
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PRÉSENTATION (Frontend) — TypeScript + React + xterm.js │
|
||||||
|
│ features/* · ui-ports (gateways) · tauri-adapters (invoke/listen) │
|
||||||
|
└───────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ IPC Tauri (commands ⇄ events, JSON)
|
||||||
|
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||||
|
│ PRÉSENTATION (Backend) — crate `app-tauri` (DRIVING ADAPTER) │
|
||||||
|
│ #[tauri::command] handlers · event emitters · COMPOSITION ROOT (DI) │
|
||||||
|
│ PTY byte-stream bridge ⇄ xterm.js │
|
||||||
|
└───────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ appels de use cases (Arc<UseCase>)
|
||||||
|
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||||
|
│ APPLICATION — crate `application` │
|
||||||
|
│ Use cases / services · DTOs · orchestration · transactions métier │
|
||||||
|
│ Dépend UNIQUEMENT des ports (traits) du domaine │
|
||||||
|
└───────────────────────────────┬───────────────────────────────────────┘
|
||||||
|
│ implémente / consomme
|
||||||
|
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||||
|
│ DOMAINE — crate `domain` (PUR, sans I/O) │
|
||||||
|
│ Entities · Value Objects · Invariants · PORTS (traits) · DomainEvents │
|
||||||
|
└───────────────────────────────▲───────────────────────────────────────┘
|
||||||
|
│ implémentent les ports (DRIVEN ADAPTERS)
|
||||||
|
┌───────────────────────────────┴───────────────────────────────────────┐
|
||||||
|
│ INFRASTRUCTURE — crate `infrastructure` │
|
||||||
|
│ portable-pty · git2 · russh/ssh2 · wsl.exe · fs local · md/json store │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontière IPC Tauri — deux directions
|
||||||
|
|
||||||
|
- **Commands (Frontend → Backend, request/response)** : `invoke("create_project", {...})`. Le handler `#[tauri::command]` désérialise le DTO, appelle le use case, renvoie un `Result<DTO, ErrorDTO>`. **Stateless** côté forme : tout l'état vit dans des services managés via `tauri::State`.
|
||||||
|
- **Events (Backend → Frontend, push)** : flux PTY (octets/base64), changements de statut d'agent, fin de processus, progrès git, drift de template détecté. Émis via `app_handle.emit(...)` / channels Tauri. L'`EventBus` domaine est relayé vers ces events Tauri par un adapter dans `app-tauri`.
|
||||||
|
|
||||||
|
> **Décision** : le flux PTY haute fréquence passe par des **Tauri Channels** (`tauri::ipc::Channel`) plutôt que des events globaux, pour la perf et l'isolement par session terminal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Modèle de domaine
|
||||||
|
|
||||||
|
### 3.1 Vue d'ensemble (relations)
|
||||||
|
|
||||||
|
```
|
||||||
|
Workspace 1───* Window 1───* Tab 1───1 Project
|
||||||
|
│ │
|
||||||
|
│ 1 ├──* Agent ─────? AgentTemplate (origine)
|
||||||
|
│ │ │ 1
|
||||||
|
│ 1 │ └──1 AgentProfile (runtime IA, par réf id)
|
||||||
|
LayoutTree ├──1 GitRepository
|
||||||
|
(LayoutNode récursif) ├──1 RemoteHost (Local | Ssh | Wsl)
|
||||||
|
│ feuilles └──1 AgentManifest (.ideai/agents.json)
|
||||||
|
▼
|
||||||
|
TerminalSession 1───? Agent (si lancé par un agent)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Entités & Value Objects (avec invariants)
|
||||||
|
|
||||||
|
**`ProjectId`, `AgentId`, `TemplateId`, `ProfileId`, `SessionId`, `WindowId`, `TabId`, `NodeId`** — VO `newtype(Uuid)` ou string typée. Invariant : non vide, immuable.
|
||||||
|
|
||||||
|
**`Project`** (entité, racine d'agrégat projet)
|
||||||
|
- Champs : `id`, `name`, `root: ProjectPath`, `remote: RemoteRef`, `created_at`.
|
||||||
|
- Invariants : `root` doit être un chemin **absolu et valide pour son `RemoteRef`** ; deux projets ne peuvent partager le même `(remote, root)`.
|
||||||
|
|
||||||
|
**`ProjectPath`** (VO) — chemin absolu normalisé, conscient de la plateforme cible (POSIX vs Windows vs WSL `/mnt/...`).
|
||||||
|
|
||||||
|
**`Agent`** (entité)
|
||||||
|
- Champs : `id`, `name`, `context: AgentContextRef` (chemin du `.md` dans `.ideai/`), `profile_id: ProfileId`, `origin: AgentOrigin` (`Scratch` | `FromTemplate { template_id, synced_version }`), `synchronized: bool`.
|
||||||
|
- Invariants : `synchronized == true` ⇒ `origin == FromTemplate{..}` (on ne peut pas synchroniser un agent créé from scratch). `context` doit exister à l'activation. `profile_id` doit référencer un `AgentProfile` connu.
|
||||||
|
|
||||||
|
**`AgentTemplate`** (entité, store global)
|
||||||
|
- Champs : `id`, `name`, `content_md: MarkdownDoc`, `version: TemplateVersion`, `default_profile_id`.
|
||||||
|
- Invariants : `version` **monotone croissante** ; toute modification du `content_md` ⇒ `version + 1` (voir §8).
|
||||||
|
|
||||||
|
**`AgentProfile`** (entité de config runtime IA — le port `AgentRuntime` est paramétré par elle)
|
||||||
|
- Champs : `id`, `name`, `command: String`, `args: Vec<String>`, `context_injection: ContextInjection`, `detect: Option<String>`, `cwd_template: String` (vaut toujours `"{agentRunDir}"` — voir §9.1 et §14.1).
|
||||||
|
- Invariants : `command` non vide ; cohérence de `ContextInjection` (voir VO ci-dessous).
|
||||||
|
|
||||||
|
**`ContextInjection`** (VO, enum — cœur du moteur IA flexible)
|
||||||
|
```
|
||||||
|
ContextInjection =
|
||||||
|
| ConventionFile { target: String } // ex. "CLAUDE.md" / "AGENTS.md" / "GEMINI.md"
|
||||||
|
| Flag { flag: String } // ex. "--context-file {path}" ou "-f"
|
||||||
|
| Stdin // pipe du contenu md sur stdin
|
||||||
|
| Env { var: String } // ex. "AGENT_CONTEXT_FILE"
|
||||||
|
```
|
||||||
|
- Invariants : `ConventionFile.target` est un nom de fichier relatif (pas de `..`, pas absolu) ; `Env.var` est un identifiant d'env valide ; `Flag.flag` non vide.
|
||||||
|
|
||||||
|
**`TerminalSession`** (entité)
|
||||||
|
- Champs : `id`, `node_id` (cellule du layout qui l'héberge), `cwd: ProjectPath`, `kind: SessionKind` (`Plain` | `Agent { agent_id }`), `pty_size: PtySize { rows, cols }`, `status` (`Starting|Running|Exited{code}`).
|
||||||
|
- Invariants : une cellule (feuille de layout) héberge **au plus une** `TerminalSession` active. `pty_size.rows>0 && cols>0`.
|
||||||
|
|
||||||
|
**`LayoutNode` / `LayoutTree`** (VO récursif — voir §7 pour le détail complet)
|
||||||
|
- Invariants : poids relatifs strictement positifs ; somme normalisable ; pas de fusion qui chevauche deux conteneurs distincts ; un `Leaf` référence 0 ou 1 `SessionId`.
|
||||||
|
|
||||||
|
**`RemoteHost`** (VO de stratégie de localisation — abstrait Local/SSH/WSL)
|
||||||
|
```
|
||||||
|
RemoteRef =
|
||||||
|
| Local
|
||||||
|
| Ssh { host, port, user, auth: SshAuth, remote_root }
|
||||||
|
| Wsl { distro: String }
|
||||||
|
```
|
||||||
|
- Invariants : `Ssh.port` ∈ 1..=65535 ; `Wsl.distro` non vide ; pour `Ssh`/`Wsl`, les chemins projet sont interprétés côté distant.
|
||||||
|
|
||||||
|
**`GitRepository`** (entité)
|
||||||
|
- Champs : `project_id`, `root`, `current_branch`, `is_dirty`.
|
||||||
|
- Invariants : `root` contient (ou contiendra après init) un `.git`. État dérivé, rafraîchi via le port.
|
||||||
|
|
||||||
|
**`AgentManifest`** (entité — image en mémoire de `.ideai/agents.json`)
|
||||||
|
- Champs : `entries: Vec<ManifestEntry { agent_id, md_path, template_id?, synchronized, synced_template_version? }>`.
|
||||||
|
- Invariants : `synchronized ⇒ template_id.is_some() && synced_template_version.is_some()` ; `md_path` unique ; cohérence avec les `Agent` chargés.
|
||||||
|
|
||||||
|
**`Workspace` / `Window` / `Tab`** (entités de présentation persistée)
|
||||||
|
- `Workspace` = ensemble des fenêtres d'une session utilisateur.
|
||||||
|
- `Window` = fenêtre OS ; possède un `LayoutTree` **par onglet actif** et une liste de `Tab`.
|
||||||
|
- `Tab` = onglet ⇔ **un `Project`** (1:1).
|
||||||
|
- Invariants : un `Project` ouvert apparaît dans **exactement un** `Tab` à la fois (le drag déplace, ne duplique pas) ; un `Window` a ≥ 1 `Tab` ou est fermée.
|
||||||
|
|
||||||
|
**`Skill`** (entité)
|
||||||
|
- Champs : `id`, `name`, `content_md: MarkdownDoc`, `scope: SkillScope` (`Global` | `Project`).
|
||||||
|
- Invariants : `name` non vide ; `content_md` non vide.
|
||||||
|
- Un agent référence 0..N skills (dans l'`AgentManifest`). Les skills assignés sont injectés dans son convention file à l'activation.
|
||||||
|
|
||||||
|
**`DomainEvent`** (enum) — `ProjectCreated`, `AgentLaunched`, `AgentExited`, `TemplateUpdated`, `AgentDriftDetected`, `SkillAssigned`, `LayoutChanged`, `RemoteConnected`, `GitStateChanged`, `PtyOutput{session_id, bytes}`, `OrchestratorRequest{requester_id, action}` (ce dernier souvent court-circuité vers un Channel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ports (traits du domaine)
|
||||||
|
|
||||||
|
> Signatures **conceptuelles** (Rust idiomatique, `async` via `async_trait` ou retours `Future` ; erreurs typées par port). « Consommé par » = use cases. « Implémenté par » = adapters de §5.
|
||||||
|
|
||||||
|
### `AgentRuntime`
|
||||||
|
- **Rôle** : lancer/piloter la CLI d'une IA selon un `AgentProfile`, en gérant l'injection du contexte `.md`.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait AgentRuntime {
|
||||||
|
fn detect(&self, profile: &AgentProfile) -> Result<bool, RuntimeError>;
|
||||||
|
fn prepare_invocation(&self, profile: &AgentProfile, ctx: &PreparedContext, cwd: &ProjectPath)
|
||||||
|
-> Result<SpawnSpec, RuntimeError>; // commande + args + plan d'injection (fichier/flag/stdin/env)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `LaunchAgent`, `DetectProfilesUseCase` (first-run).
|
||||||
|
- **Implémenté par** : `CliAgentRuntime` (un seul adapter générique piloté par le profil déclaratif — c'est l'**Open/Closed**). La diversité des IA = données, pas code.
|
||||||
|
|
||||||
|
### `PtyPort` (alias domaine de `TerminalSessionPort`)
|
||||||
|
- **Rôle** : ouvrir un pseudo-terminal, lire/écrire, redimensionner, tuer.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait PtyPort {
|
||||||
|
async fn spawn(&self, spec: SpawnSpec, size: PtySize) -> Result<PtyHandle, PtyError>;
|
||||||
|
fn write(&self, h: &PtyHandle, data: &[u8]) -> Result<(), PtyError>;
|
||||||
|
fn resize(&self, h: &PtyHandle, size: PtySize) -> Result<(), PtyError>;
|
||||||
|
fn subscribe_output(&self, h: &PtyHandle) -> OutputStream; // flux d'octets
|
||||||
|
async fn kill(&self, h: &PtyHandle) -> Result<ExitStatus, PtyError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `OpenTerminal`, `LaunchAgent`, `CloseTerminal`.
|
||||||
|
- **Implémenté par** : `PortablePtyAdapter` (local), `SshPtyAdapter` (PTY distant via russh exec/shell), `WslPtyAdapter` (PTY via `wsl.exe`). Sélection par stratégie `RemoteRef` (Liskov).
|
||||||
|
|
||||||
|
### `RemoteHost`
|
||||||
|
- **Rôle** : abstraction de la **localisation d'exécution** (local / SSH / WSL) : exécuter une commande, ouvrir un PTY, accéder au FS, dans le bon contexte.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait RemoteHost {
|
||||||
|
fn kind(&self) -> RemoteKind;
|
||||||
|
async fn connect(&self) -> Result<(), RemoteError>;
|
||||||
|
fn file_system(&self) -> Arc<dyn FileSystem>;
|
||||||
|
fn process_spawner(&self) -> Arc<dyn ProcessSpawner>;
|
||||||
|
fn pty(&self) -> Arc<dyn PtyPort>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : tous les use cases qui touchent un projet (résolvent leurs ports via le `RemoteHost` du projet → **transparence local/distant**).
|
||||||
|
- **Implémenté par** : `LocalHost`, `SshHost` (russh/ssh2), `WslHost` (wsl.exe). C'est la **stratégie** qui unifie les 3 modes.
|
||||||
|
|
||||||
|
### `ProcessSpawner`
|
||||||
|
- **Rôle** : lancer un process **non interactif** et récupérer sortie/exit (ex. `detect`, commandes git hors libgit2, scripts).
|
||||||
|
- **Signature** : `async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError>;`
|
||||||
|
- **Consommé par** : `DetectProfilesUseCase`, services divers.
|
||||||
|
- **Implémenté par** : `LocalProcessSpawner`, `SshProcessSpawner`, `WslProcessSpawner`.
|
||||||
|
|
||||||
|
### `FileSystem`
|
||||||
|
- **Rôle** : lecture/écriture/listing/symlink, neutre vis-à-vis de la localisation.
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait FileSystem {
|
||||||
|
async fn read(&self, p: &RemotePath) -> Result<Vec<u8>, FsError>;
|
||||||
|
async fn write(&self, p: &RemotePath, data: &[u8]) -> Result<(), FsError>;
|
||||||
|
async fn exists(&self, p: &RemotePath) -> Result<bool, FsError>;
|
||||||
|
async fn create_dir_all(&self, p: &RemotePath) -> Result<(), FsError>;
|
||||||
|
async fn list(&self, p: &RemotePath) -> Result<Vec<DirEntry>, FsError>;
|
||||||
|
async fn symlink(&self, src: &RemotePath, dst: &RemotePath) -> Result<(), FsError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `AgentContextStore`, `ProjectStore`, injection `conventionFile`, etc.
|
||||||
|
- **Implémenté par** : `LocalFileSystem` (std::fs/tokio::fs), `SshFileSystem` (SFTP), `WslFileSystem` (via `wsl.exe` ou chemins `\\wsl$`).
|
||||||
|
|
||||||
|
### `TemplateStore`
|
||||||
|
- **Rôle** : CRUD des `AgentTemplate` dans le store global IDE + versioning.
|
||||||
|
- **Signature** : `list / get / save / delete / bump_version`.
|
||||||
|
- **Consommé par** : `CreateTemplate`, `UpdateTemplate`, `CreateAgentFromTemplate`, `SyncAgentWithTemplate`.
|
||||||
|
- **Implémenté par** : `FsTemplateStore` (md + index json dans le dossier de données app).
|
||||||
|
|
||||||
|
### `ProjectStore`
|
||||||
|
- **Rôle** : persistance de la liste des projets connus, workspaces, windows, tabs, layouts.
|
||||||
|
- **Signature** : `list_projects / load_project / save_project / save_workspace / load_workspace`.
|
||||||
|
- **Consommé par** : `CreateProject`, `OpenProject`, persistance fenêtres/onglets/layout.
|
||||||
|
- **Implémenté par** : `FsProjectStore` (json dans données app pour le registre ; layout par projet dans `.ideai/`).
|
||||||
|
|
||||||
|
### `AgentContextStore`
|
||||||
|
- **Rôle** : lire/écrire les `.md` d'agents **et** le manifeste `.ideai/agents.json` (au sein du projet, via le `FileSystem` du `RemoteHost`).
|
||||||
|
- **Signature** :
|
||||||
|
```rust
|
||||||
|
trait AgentContextStore {
|
||||||
|
async fn read_context(&self, project: &Project, agent: &AgentId) -> Result<MarkdownDoc, StoreError>;
|
||||||
|
async fn write_context(&self, project: &Project, agent: &AgentId, md: &MarkdownDoc) -> Result<(), StoreError>;
|
||||||
|
async fn load_manifest(&self, project: &Project) -> Result<AgentManifest, StoreError>;
|
||||||
|
async fn save_manifest(&self, project: &Project, m: &AgentManifest) -> Result<(), StoreError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Consommé par** : `CreateAgent*`, `LaunchAgent`, `SyncAgentWithTemplate`.
|
||||||
|
- **Implémenté par** : `IdeaiContextStore` (compose `FileSystem`, écrit `.ideai/`).
|
||||||
|
|
||||||
|
### `GitRepository`
|
||||||
|
- **Rôle** : opérations git du projet.
|
||||||
|
- **Signature** : `status / stage / unstage / commit / branches / checkout / current_branch / diff / log / pull / push / clone / init`.
|
||||||
|
- **Consommé par** : use cases Git.
|
||||||
|
- **Implémenté par** : `Git2Repository` (libgit2, local) ; sur SSH/WSL, `RemoteGitRepository` délègue à git CLI via `ProcessSpawner` quand libgit2 ne peut pas atteindre le FS distant (point ouvert §13).
|
||||||
|
|
||||||
|
### `EventBus`
|
||||||
|
- **Rôle** : publier/souscrire les `DomainEvent` (découple émetteurs et présentation).
|
||||||
|
- **Signature** : `fn publish(&self, e: DomainEvent); fn subscribe(&self) -> EventStream;`
|
||||||
|
- **Consommé par** : tous use cases (publient) ; l'adapter Tauri (souscrit → relaye en events/channels IPC).
|
||||||
|
- **Implémenté par** : `TokioBroadcastEventBus` (in-process), relayé par `TauriEventRelay`.
|
||||||
|
|
||||||
|
### `Clock` & `IdGenerator` (ports utilitaires — testabilité)
|
||||||
|
- **Rôle** : éliminer le non-déterminisme (`now()`, `uuid`) du domaine/application.
|
||||||
|
- **Implémenté par** : `SystemClock` / `UuidGenerator` (prod), `FixedClock` / `SeqIdGenerator` (tests).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Adapters (impl concrètes par port)
|
||||||
|
|
||||||
|
| Port | Adapter(s) | Techno | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `AgentRuntime` | `CliAgentRuntime` | piloté par `AgentProfile` | Construit `SpawnSpec` + plan d'injection. Un seul adapter, N profils. |
|
||||||
|
| `PtyPort` | `PortablePtyAdapter` | portable-pty | Local. Stream octets → Channel Tauri. |
|
||||||
|
| | `SshPtyAdapter` | russh (channel shell/exec + pty req) | Distant SSH. |
|
||||||
|
| | `WslPtyAdapter` | `wsl.exe -d <distro>` + portable-pty | PTY dans la distro. |
|
||||||
|
| `RemoteHost` | `LocalHost` / `SshHost` / `WslHost` | — / russh,ssh2 / wsl.exe | Stratégie ; fabrique FS/Spawner/PTY adaptés. |
|
||||||
|
| `ProcessSpawner` | `LocalProcessSpawner` | std/tokio `Command` | |
|
||||||
|
| | `SshProcessSpawner` | russh exec | |
|
||||||
|
| | `WslProcessSpawner` | `wsl.exe` | |
|
||||||
|
| `FileSystem` | `LocalFileSystem` | tokio::fs | |
|
||||||
|
| | `SshFileSystem` | SFTP (ssh2/russh-sftp) | |
|
||||||
|
| | `WslFileSystem` | `\\wsl$\` / `wsl.exe cat`… | |
|
||||||
|
| `TemplateStore` | `FsTemplateStore` | tokio::fs + serde_json | Dossier données app. |
|
||||||
|
| `ProjectStore` | `FsProjectStore` | tokio::fs + serde_json | Registre projets + workspace. |
|
||||||
|
| `AgentContextStore` | `IdeaiContextStore` | compose `FileSystem` | Écrit `.ideai/`. |
|
||||||
|
| `GitRepository` | `Git2Repository` | git2 | Local. |
|
||||||
|
| | `RemoteGitRepository` | git CLI via `ProcessSpawner` | SSH/WSL fallback. |
|
||||||
|
| `EventBus` | `TokioBroadcastEventBus` (+ `TauriEventRelay`) | tokio::broadcast | Relais vers IPC. |
|
||||||
|
| `Clock`/`IdGenerator` | `SystemClock`/`UuidGenerator` | std/uuid | Mocks en test. |
|
||||||
|
|
||||||
|
**Adapters entrants (driving)** : handlers `#[tauri::command]` (frontend → app) + `TauriEventRelay` (app → frontend). Côté UI : `tauri-adapters` implémentant les gateways TS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Use cases / services applicatifs
|
||||||
|
|
||||||
|
> Chaque use case : un struct `XxxUseCase` portant ses ports en `Arc<dyn Port>`, une méthode `execute(input: XxxInput) -> Result<XxxOutput, AppError>`. **Single Responsibility**. Aucune dépendance à Tauri.
|
||||||
|
|
||||||
|
| Use case | Rôle | Ports consommés |
|
||||||
|
|---|---|---|
|
||||||
|
| `CreateProject` | Crée un projet (project root), init `.ideai/`, registre. | `ProjectStore`, `FileSystem`, `IdGenerator`, `EventBus` |
|
||||||
|
| `OpenProject` | Charge projet, manifeste, layout, résout `RemoteHost`. | `ProjectStore`, `AgentContextStore`, `RemoteHost` |
|
||||||
|
| `CloseProject` / `CloseTab` | Persiste l'état, libère PTYs. | `ProjectStore`, `PtyPort`, `EventBus` |
|
||||||
|
| `DetectProfiles` (first-run) | Teste `detect` de chaque profil candidat. | `AgentRuntime`, `ProcessSpawner` |
|
||||||
|
| `ConfigureProfiles` | Enregistre profils choisis/édités/custom. | `TemplateStore`/profile store, `FileSystem` |
|
||||||
|
| `CreateAgentFromScratch` | Crée agent + `.md`, met à jour manifeste. | `AgentContextStore`, `IdGenerator` |
|
||||||
|
| `CreateAgentFromTemplate` | Copie le `content_md` du template → agent ; lie origine + version + `synchronized`. | `TemplateStore`, `AgentContextStore` |
|
||||||
|
| `UpdateTemplate` | Modifie un template, **bump version**, signale drift aux agents liés. | `TemplateStore`, `EventBus` |
|
||||||
|
| `DetectAgentDrift` | Compare `synced_template_version` vs `template.version`. | `TemplateStore`, `AgentContextStore` |
|
||||||
|
| `SyncAgentWithTemplate` | Applique la MAJ template→agent si `synchronized`. | `TemplateStore`, `AgentContextStore`, `EventBus` |
|
||||||
|
| `LaunchAgent` | Résout profil+contexte, prépare injection, ouvre cellule PTY au bon `cwd`, spawn CLI. | `AgentRuntime`, `AgentContextStore`, `RemoteHost`→`PtyPort`/`FileSystem`, `EventBus` |
|
||||||
|
| `OpenTerminal` | Ouvre un PTY simple dans une cellule. | `RemoteHost`→`PtyPort`, `EventBus` |
|
||||||
|
| `WriteToTerminal` / `ResizeTerminal` / `CloseTerminal` | I/O PTY. | `PtyPort` |
|
||||||
|
| `MutateLayout` (split/merge/resize/move) | Applique une opération sur le `LayoutTree` (logique **pure** dans le domaine, persistée ici). | `ProjectStore` (persistance) |
|
||||||
|
| `ConnectRemote` (SSH/WSL) | Établit la connexion, valide l'accès au root. | `RemoteHost`, `FileSystem` |
|
||||||
|
| `MoveTabToNewWindow` | Détache un onglet → nouvelle fenêtre (réaffectation `WindowId`). | `ProjectStore`, `EventBus` |
|
||||||
|
| Use cases Git | `GitStatus`, `GitCommit`, `GitCheckout`, `GitPush`, … | `GitRepository`, `EventBus` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Modèle de layout terminal (grille tableur récursive + fusion)
|
||||||
|
|
||||||
|
### 7.1 Structure de données
|
||||||
|
|
||||||
|
La grille « type tableur, lignes/colonnes imbriquées indépendamment + fusion » est modélisée par un **arbre de splits récursif** où chaque conteneur définit son propre découpage. La **fusion** est obtenue nativement : fusionner = ne pas subdiviser une zone (un `Leaf` couvre plusieurs « cellules visuelles » d'un parent voisin). Pour le cas Excel pur (fusion arbitraire chevauchant la grille), on superpose un modèle **GridContainer** avec spans.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum LayoutNode {
|
||||||
|
Leaf(LeafCell),
|
||||||
|
Split(SplitContainer),
|
||||||
|
Grid(GridContainer),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LeafCell {
|
||||||
|
id: NodeId,
|
||||||
|
session: Option<SessionId>, // 0 ou 1 terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SplitContainer { // découpage simple binaire/n-aire pondéré
|
||||||
|
id: NodeId,
|
||||||
|
direction: Direction, // Row (colonnes) | Column (lignes)
|
||||||
|
children: Vec<WeightedChild>, // ordre = gauche→droite / haut→bas
|
||||||
|
}
|
||||||
|
struct WeightedChild { node: LayoutNode, weight: f32 } // poids = part redimensionnable
|
||||||
|
|
||||||
|
struct GridContainer { // grille tableur avec fusion (spans)
|
||||||
|
id: NodeId,
|
||||||
|
col_weights: Vec<f32>, // largeurs de colonnes
|
||||||
|
row_weights: Vec<f32>, // hauteurs de lignes
|
||||||
|
cells: Vec<GridCell>, // placements avec spans (fusion)
|
||||||
|
}
|
||||||
|
struct GridCell {
|
||||||
|
node: LayoutNode, // récursif : une cellule peut re-contenir un Split/Grid
|
||||||
|
row: u16, col: u16,
|
||||||
|
row_span: u16, // ≥1 ; >1 = cellules fusionnées verticalement
|
||||||
|
col_span: u16, // ≥1 ; >1 = cellules fusionnées horizontalement
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Lignes/colonnes indépendantes par zone** : chaque `SplitContainer`/`GridContainer` a ses propres poids ⇒ pas de grille uniforme rigide.
|
||||||
|
- **Imbrication** : un enfant peut être un nouveau `Split`/`Grid` ⇒ « N colonnes dans une ligne, M lignes dans une colonne » de façon arbitraire.
|
||||||
|
- **Fusion** : `row_span`/`col_span` dans `GridContainer` (modèle tableur fidèle) **ou** simplement un `Leaf` plus grand via `SplitContainer` (cas courant). Le domaine supporte les deux ; l'UI choisit la représentation selon l'interaction.
|
||||||
|
|
||||||
|
### 7.2 Invariants (validés dans le domaine, testables sans I/O)
|
||||||
|
|
||||||
|
- Tous les `weight > 0`. Les poids sont **relatifs** (l'UI normalise pour le rendu).
|
||||||
|
- Dans un `GridContainer` : aucune superposition de spans ; toute la surface couverte ; `row+row_span ≤ rows`, `col+col_span ≤ cols`.
|
||||||
|
- Un `SessionId` n'apparaît que dans **un seul** `Leaf`.
|
||||||
|
- Les opérations `split`, `merge`, `resize`, `move` sont des **fonctions pures** `LayoutTree -> Result<LayoutTree, LayoutError>` (immutabilité ⇒ testabilité, undo/redo facile).
|
||||||
|
|
||||||
|
### 7.3 Sérialisation & persistance
|
||||||
|
|
||||||
|
- Sérialisé en **JSON** (serde, `tag`/`content` pour l'enum) → `.ideai/layout.json` (par projet, donc voyage avec le projet, y compris distant).
|
||||||
|
- Le `Workspace`/`Window`/`Tab` (organisation des fenêtres OS) est persisté côté **store global IDE** (machine-local, pas dans le projet) car lié à l'écran de l'utilisateur, pas au code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Synchronisation template → agents
|
||||||
|
|
||||||
|
### 8.1 Versioning
|
||||||
|
|
||||||
|
- `AgentTemplate.version: u64` monotone. **`UpdateTemplate` incrémente** la version à chaque changement de `content_md`. Un hash du contenu (`content_hash`) est aussi stocké pour détecter les éditions hors-app.
|
||||||
|
- Chaque `ManifestEntry` d'agent lié garde `synced_template_version` = version du template **au dernier sync réussi**.
|
||||||
|
|
||||||
|
### 8.2 Détection de drift
|
||||||
|
|
||||||
|
```
|
||||||
|
drift(agent) =
|
||||||
|
agent.synchronized
|
||||||
|
&& agent.origin == FromTemplate{ template_id, .. }
|
||||||
|
&& template_store.get(template_id).version > entry.synced_template_version
|
||||||
|
```
|
||||||
|
`DetectAgentDrift` est lancé à `OpenProject` et après chaque `UpdateTemplate` ; émet `AgentDriftDetected { agent_id, from, to }` → badge UI.
|
||||||
|
|
||||||
|
### 8.3 Application de la MAJ (`SyncAgentWithTemplate`)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Charger template (version courante) + manifeste projet.
|
||||||
|
2. Pour chaque agent ciblé avec synchronized==true :
|
||||||
|
a. Stratégie de MAJ = REMPLACEMENT du .md par content_md du template
|
||||||
|
(le contexte d'un agent synchronisé est "possédé" par le template).
|
||||||
|
→ Variante future : merge 3-way si l'agent a un bloc local marqué.
|
||||||
|
b. write_context(agent, template.content_md)
|
||||||
|
c. entry.synced_template_version = template.version
|
||||||
|
3. save_manifest. publish(AgentSynced{..}).
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Agents non synchronisés
|
||||||
|
|
||||||
|
- `synchronized == false` : ne reçoivent **jamais** de MAJ auto. Ils gardent leur `.md` libre. On peut afficher « une nouvelle version du template existe » (info) mais aucune écriture n'a lieu sans action explicite (qui basculerait `synchronized` ou ferait un sync ponctuel one-shot).
|
||||||
|
- Agents `Scratch` : aucun lien template, hors périmètre de sync.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Stockage & arborescence des fichiers
|
||||||
|
|
||||||
|
### 9.1 Dans le projet — `.ideai/` (voyage avec le code, versionnable)
|
||||||
|
|
||||||
|
```
|
||||||
|
<project_root>/
|
||||||
|
├── .ideai/
|
||||||
|
│ ├── agents.json # AgentManifest (mapping md ↔ template ↔ sync ↔ version)
|
||||||
|
│ ├── layout.json # LayoutTree de l'onglet (sérialisé)
|
||||||
|
│ ├── project.json # méta projet local (nom, profil par défaut, remote ref)
|
||||||
|
│ ├── agents/
|
||||||
|
│ │ ├── reviewer.md # contexte d'un agent de projet
|
||||||
|
│ │ ├── backend-dev.md
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── skills/
|
||||||
|
│ │ ├── code-review.md # contexte d'un skill (voir §14.2)
|
||||||
|
│ │ ├── simplify.md
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── run/
|
||||||
|
│ ├── <agent-id>/ # cwd isolé par agent actif (créé à l'activation)
|
||||||
|
│ │ └── CLAUDE.md # fichier de convention généré par IdeA (profil-dépendant)
|
||||||
|
│ └── ...
|
||||||
|
└── (aucun CLAUDE.md/AGENTS.md/GEMINI.md à la racine — jamais — voir §14.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schéma `agents.json`** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"id": "a3f1...",
|
||||||
|
"name": "Backend Dev",
|
||||||
|
"md": "agents/backend-dev.md",
|
||||||
|
"profileId": "claude-code",
|
||||||
|
"origin": { "type": "fromTemplate", "templateId": "tpl-backend", "syncedTemplateVersion": 4 },
|
||||||
|
"synchronized": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b7c2...",
|
||||||
|
"name": "Ad-hoc",
|
||||||
|
"md": "agents/adhoc.md",
|
||||||
|
"profileId": "codex-cli",
|
||||||
|
"origin": { "type": "scratch" },
|
||||||
|
"synchronized": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Store global IDE (données app, hors projet, machine-local)
|
||||||
|
|
||||||
|
Emplacement résolu via Tauri path API (`AppData`/`~/.local/share/IdeA`/`~/Library/Application Support/IdeA`).
|
||||||
|
|
||||||
|
```
|
||||||
|
<app_data_dir>/IdeA/
|
||||||
|
├── profiles.json # AgentProfile[] configurés (first-run + custom + édités)
|
||||||
|
├── settings.json # préférences IDE
|
||||||
|
├── workspace.json # Workspace/Window/Tab + quel projet dans quel onglet (machine-local)
|
||||||
|
└── templates/
|
||||||
|
├── index.json # [{id, name, version, contentHash, defaultProfileId}]
|
||||||
|
└── md/
|
||||||
|
├── tpl-backend.md
|
||||||
|
├── tpl-reviewer.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schéma `profiles.json` (item)** : exactement le profil déclaratif de CONTEXT.md §9 (`id, name, command, args, contextInjection{strategy,target/flag/var}, detect, cwd`).
|
||||||
|
|
||||||
|
**Formats** : contextes & templates en **Markdown** ; tout le reste en **JSON** (serde). Pas de base de données : fichiers plats, simples, diffables, portables (AppImage friendly).
|
||||||
|
|
||||||
|
> **Note** : `.ideai/run/` contient des répertoires d'exécution éphémères (créés à l'activation, nettoyés à la fermeture). Leur contenu (convention files générés) ne doit **pas** être versionné dans git — ajouter `.ideai/run/` au `.gitignore` du projet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Arborescence du repo
|
||||||
|
|
||||||
|
### 10.1 Décision : workspace Cargo **multi-crate**
|
||||||
|
|
||||||
|
**Multi-crate** retenu (vs mono-crate) pour **forcer** la règle de dépendance à la compilation : le crate `domain` ne peut littéralement pas dépendre de `infrastructure` si ce n'est pas dans son `Cargo.toml`. C'est la garantie mécanique de l'hexagonal (mieux qu'une convention). Coût : un peu de cérémonie de workspace — acceptable et même souhaitable ici vu le découpage en lots/agents (§12).
|
||||||
|
|
||||||
|
```
|
||||||
|
IdeA/
|
||||||
|
├── Cargo.toml # [workspace] members
|
||||||
|
├── ARCHITECTURE.md
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── crates/
|
||||||
|
│ ├── domain/ # PUR : entities, VO, ports (traits), domain events, layout logic
|
||||||
|
│ │ └── src/{project,agent,template,profile,terminal,layout,remote,git,ports,events}.rs
|
||||||
|
│ ├── application/ # use cases, DTOs, AppError ; dépend de domain
|
||||||
|
│ │ └── src/{project,agent,template,terminal,layout,remote,git}/
|
||||||
|
│ ├── infrastructure/ # adapters ; dépend de domain (+ application pour DTO si besoin)
|
||||||
|
│ │ └── src/{pty,fs,process,remote,git,store,runtime,eventbus}/
|
||||||
|
│ └── app-tauri/ # binaire Tauri : commands, events, COMPOSITION ROOT (DI)
|
||||||
|
│ ├── src/{commands,events,state,main.rs}
|
||||||
|
│ ├── tauri.conf.json
|
||||||
|
│ ├── build.rs
|
||||||
|
│ └── icons/, bundle (NSIS + AppImage)
|
||||||
|
├── frontend/ # TypeScript + React (Vite)
|
||||||
|
│ ├── package.json, vite.config.ts, index.html
|
||||||
|
│ └── src/
|
||||||
|
│ ├── domain/ # types & logique de vue purs (miroir DTO, calc layout)
|
||||||
|
│ ├── ports/ # gateways TS (interfaces) : AgentGateway, TerminalGateway, ...
|
||||||
|
│ ├── adapters/ # impl gateways via @tauri-apps/api (invoke/listen/Channel)
|
||||||
|
│ │ └── mock/ # impl mock pour dev/test/storybook
|
||||||
|
│ ├── features/ # par feature : projects, agents, templates, terminals, layout, git, remote, first-run
|
||||||
|
│ │ └── <feature>/{components,hooks,store,index.ts}
|
||||||
|
│ ├── shared/ # ui kit, xterm wrapper, design system
|
||||||
|
│ └── app/ # bootstrap, routing, providers (DI des adapters)
|
||||||
|
└── docs/ # ADRs, schémas
|
||||||
|
```
|
||||||
|
|
||||||
|
`app-tauri` = **seul** endroit qui connaît tous les crates : il instancie les adapters concrets et injecte dans les use cases (composition root). Personne d'autre ne fait de `new ConcreteAdapter`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Stratégie de tests
|
||||||
|
|
||||||
|
| Couche | Type de test | Comment / où |
|
||||||
|
|---|---|---|
|
||||||
|
| `domain` | **Unitaires purs** (sans I/O, sans async) | `#[cfg(test)] mod tests` par module. Invariants d'entités, opérations de layout (split/merge/resize), détection de drift, validation `ContextInjection`. Déterministe via `FixedClock`/`SeqIdGenerator`. |
|
||||||
|
| `application` | **Unitaires avec ports mockés** | Chaque use case testé avec des **mocks de ports** (`mockall` ou fakes manuels). Ex. `LaunchAgent` vérifie qu'il appelle `prepare_invocation` puis `pty.spawn` avec le bon `cwd` et plan d'injection. **Aucun vrai PTY/FS/git.** |
|
||||||
|
| `infrastructure` | **Tests d'intégration ciblés** | Par adapter : `LocalFileSystem` sur tmpdir, `Git2Repository` sur repo temporaire, `PortablePtyAdapter` lance `echo`. SSH/WSL : tests `#[ignore]` gated derrière feature/env (CI conditionnelle). |
|
||||||
|
| `app-tauri` | Tests des commands (mapping DTO ↔ use case) | Wiring testé avec use cases réels + adapters in-memory. |
|
||||||
|
| Frontend `domain`/`ports` | **Vitest** (unitaires purs) | Logique de vue, calc tailles cellules, réducteurs de state. |
|
||||||
|
| Frontend `features` | **React Testing Library** + **gateways mock** | Composants testés avec adapters mock ⇒ **sans backend**. |
|
||||||
|
| E2E (plus tard) | Playwright / `tauri-driver` | Smoke tests des parcours clés. |
|
||||||
|
|
||||||
|
**Clé de testabilité** : grâce aux **ports**, le domaine et l'application se testent **100 % sans I/O**. C'est l'argument central de l'hexagonal et le socle du cycle dev↔test (chaque agent dev appairé à un agent test, cf. CONTEXT §3). Règle d'or : une feature n'est verte que quand `cargo test -p <crate>` et `vitest` passent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Découpage en lots/features livrables
|
||||||
|
|
||||||
|
> Chaque lot = périmètre autonome, validable par le cycle dev/test, confiable à **un binôme (agent dev + agent test)**. Ordonnés par dépendance.
|
||||||
|
|
||||||
|
| # | Lot | Contenu | Crates/zones |
|
||||||
|
|---|---|---|---|
|
||||||
|
| L0 | **Socle domaine & ports** | Entities, VO, **tous les traits ports**, domain events, `AppError`. Aucun adapter. | `domain` (+ ports utilitaires) |
|
||||||
|
| L1 | **Composition root & IPC** | `app-tauri` : DI, registre de commands/events, bridge PTY↔Channel, gateways TS + adapters Tauri + mocks. | `app-tauri`, `frontend/ports`+`adapters` |
|
||||||
|
| L2 | **Projets & stockage** | `CreateProject`/`OpenProject`/`CloseProject`, `FsProjectStore`, `LocalFileSystem`, init `.ideai/`. UI projets/onglets. | `application/project`, `infrastructure/{fs,store}`, `frontend/features/projects` |
|
||||||
|
| L3 | **Terminaux & PTY (local)** | `PtyPort` + `PortablePtyAdapter`, use cases terminal, wrapper xterm.js, flux Channel. | `infrastructure/pty`, `application/terminal`, `frontend/features/terminals` |
|
||||||
|
| L4 | **Layout tableur** | Logique pure `LayoutTree` (déjà en L0 partiellement), `MutateLayout`, persistance `layout.json`, UI grille redimensionnable + fusion. | `domain/layout`, `application/layout`, `frontend/features/layout` |
|
||||||
|
| L5 | **Profils IA & runtime** | `AgentProfile`, `CliAgentRuntime`, `DetectProfiles`, first-run wizard, `profiles.json`. | `infrastructure/runtime`, `application/agent`, `frontend/features/first-run` |
|
||||||
|
| L6 | **Agents & contextes** | `AgentContextStore`/`IdeaiContextStore`, CRUD agents, `LaunchAgent` (injection + spawn + cellule). | `application/agent`, `infrastructure/store`, `frontend/features/agents` |
|
||||||
|
| L7 | **Templates & synchro** | `TemplateStore`, versioning, `DetectAgentDrift`, `SyncAgentWithTemplate`. UI templates + badges drift. | `application/template`, `infrastructure/store`, `frontend/features/templates` |
|
||||||
|
| L8 | **Git** | `GitRepository`/`Git2Repository`, use cases git, UI git. | `infrastructure/git`, `application/git`, `frontend/features/git` |
|
||||||
|
| L9 | **Remote (SSH + WSL)** | `RemoteHost` stratégie, `SshHost`/`WslHost`, adapters FS/PTY/Spawner distants, `RemoteGitRepository`. UI connexion. | `infrastructure/remote`, `application/remote`, `frontend/features/remote` |
|
||||||
|
| L10 | **Fenêtres & multi-window** | `Workspace`/`Window`/`Tab`, `MoveTabToNewWindow`, drag d'onglet → nouvelle fenêtre OS Tauri. | `application`, `app-tauri`, `frontend/app` |
|
||||||
|
| L11 | **Packaging & livraison** | Tauri bundle : NSIS `setup.exe`, **AppImage** multi-distro, CI Linux+Windows. | `app-tauri`, CI |
|
||||||
|
| L12 | **Skills** | Entité `Skill`, `SkillStore`, CRUD skills global+projet, assignation agent↔skills, injection dans convention file à l'activation. UI onglet Skills. | `domain/skill`, `application/skill`, `infrastructure/store`, `frontend/features/skills` |
|
||||||
|
| L13 | **OrchestratorApi** | File-watcher `.ideai/requests/`, port `OrchestratorApi`, adapter `FsOrchestratorAdapter`, actions `spawn_agent`/`stop_agent`/`update_agent_context`. | `infrastructure/orchestrator`, `application/agent`, `app-tauri` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Décisions d'architecture figées (2026-06-06)
|
||||||
|
|
||||||
|
### 14.1 Isolation du cwd par agent — résolution de la collision de contexte
|
||||||
|
|
||||||
|
**Problème** : plusieurs agents du même profil (ex. deux instances Claude Code) sur le même project root produisaient une collision — le fichier de convention (`CLAUDE.md`, `AGENTS.md`…) est un emplacement fixe unique à la racine.
|
||||||
|
|
||||||
|
**Décision** : le cwd du PTY d'un agent n'est **jamais** le project root. C'est `.ideai/run/<agent-id>/`, un dossier créé par IdeA à l'activation et nettoyé à la fermeture.
|
||||||
|
|
||||||
|
**Convention file généré par IdeA** : IdeA écrit dans ce dossier le fichier conventionnel attendu par le profil (`CLAUDE.md`, `AGENTS.md`, etc.). Ce fichier contient :
|
||||||
|
1. La **persona/rôle** de l'agent (son `.md` dans `.ideai/agents/`).
|
||||||
|
2. Le **chemin absolu du project root** (pour que l'agent sache où opérer).
|
||||||
|
3. Les **skills actifs** assignés à cet agent (voir §14.2).
|
||||||
|
4. Une référence au **contexte projet partagé** si présent.
|
||||||
|
|
||||||
|
**Avantages** :
|
||||||
|
- Zéro collision entre agents, même N instances du même profil.
|
||||||
|
- Universel : fonctionne pour toute CLI qui lit un fichier de convention depuis son cwd — aucun flag ou commande propre à un modèle.
|
||||||
|
- Zéro dépendance à git (git est optionnel — supprimer un repo ne casse rien).
|
||||||
|
|
||||||
|
**Impact sur `AgentProfile.cwd_template`** : la valeur est toujours `"{agentRunDir}"`, jamais `"{projectRoot}"`. La connaissance du project root passe par le *contenu* du convention file, pas par le cwd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14.2 Skills — abstraction universelle de workflows réutilisables
|
||||||
|
|
||||||
|
**Définition** : un **Skill** est un workflow/comportement réutilisable qu'on peut assigner à un agent. Exemples : `code-review`, `simplify`, `run-tests`, `explain`. C'est l'équivalent universel des slash-commands de Claude Code — mais sans dépendance à la syntaxe `/command` d'un modèle particulier.
|
||||||
|
|
||||||
|
**Stockage** :
|
||||||
|
- Skills globaux (templates) : `<app_data>/IdeA/skills/` (store global IDE, réutilisables entre projets).
|
||||||
|
- Skills de projet : `.ideai/skills/<skill-name>.md` (spécifiques au projet).
|
||||||
|
|
||||||
|
**Injection** : les skills assignés à un agent sont **inclus dans son convention file** généré par IdeA au moment de l'activation. L'agent reçoit donc ses skills comme du contexte textuel — aucun mécanisme CLI propriétaire.
|
||||||
|
|
||||||
|
**Entité `Skill`** (à ajouter au domaine) :
|
||||||
|
- Champs : `id`, `name`, `content_md: MarkdownDoc`, `scope: SkillScope` (`Global` | `Project`).
|
||||||
|
- Un agent peut avoir 0..N skills assignés (stocké dans l'`AgentManifest`).
|
||||||
|
|
||||||
|
**Port `SkillStore`** : CRUD skills globaux + skills projet (compose `FileSystem`/store global selon le scope).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14.3 OrchestratorApi — spawn d'agents depuis un agent ou depuis l'UI
|
||||||
|
|
||||||
|
**Objectif** : qu'un agent orchestrateur puisse demander à IdeA de créer un nouvel agent (visible dans la grille et dans l'onglet Agents), exactement comme le ferait l'utilisateur via l'UI.
|
||||||
|
|
||||||
|
**Mécanisme** : file-watching sur `.ideai/requests/<requester-id>/`. L'orchestrateur écrit un fichier JSON de requête :
|
||||||
|
```json
|
||||||
|
{ "action": "spawn_agent", "name": "dev-backend", "profile": "claude-code", "context": "agents/dev-backend.md" }
|
||||||
|
```
|
||||||
|
IdeA détecte le fichier, exécute la même logique que `LaunchAgent` déclenché depuis l'UI, crée la cellule terminal, inscrit l'agent dans l'onglet Agents, puis supprime le fichier et écrit une réponse.
|
||||||
|
|
||||||
|
**Règle** : l'orchestrateur ne spawne jamais lui-même un process CLI — il **délègue à IdeA**. IdeA reste l'unique source de vérité du cycle de vie des agents.
|
||||||
|
|
||||||
|
**Port `OrchestratorApi`** (adapter entrant, driven by file-watcher) : surveille `.ideai/requests/`, désérialise les commandes, les traduit en appels de use cases (`LaunchAgent`, `StopAgent`…). Implémenté dans `infrastructure/orchestrator`.
|
||||||
|
|
||||||
|
**Actions supportées (v1)** : `spawn_agent`, `stop_agent`, `update_agent_context`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14.4 Git = intégration optionnelle, zéro dépendance fonctionnelle
|
||||||
|
|
||||||
|
Git est un **outil posé par-dessus l'IDE**, pas un socle. Supprimer le repo git d'un projet ne doit casser aucune feature d'IdeA (agents, terminaux, layout, skills, orchestration). Les use cases git (L8) sont un module indépendant ; rien d'autre n'en dépend. Cette contrainte s'applique à toute future décision de conception.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Risques techniques & points ouverts (spikes)
|
||||||
|
|
||||||
|
1. **PTY cross-platform** : portable-pty + xterm.js OK sur les 3 OS, mais signaux/resize/exit codes diffèrent (Windows ConPTY). **Spike** L3.
|
||||||
|
2. **AppImage multi-distro** : libgit2/openssl/glibc liés dynamiquement → risque de non-portabilité. **Spike** : vendoring statique (`git2` features, `rustls` pour russh au lieu d'OpenSSL), test sur ≥3 distros (Ubuntu/Fedora/Arch). L11.
|
||||||
|
3. **Drag d'onglet entre fenêtres Tauri** : Tauri v2 multi-webview/multi-window + DnD natif inter-fenêtres est délicat (le DnD HTML ne traverse pas les fenêtres OS). **Spike** : protocole « detach » (créer une `WebviewWindow`, transférer l'état via store + event, fermer l'onglet source). L10.
|
||||||
|
4. **Git sur FS distant** : libgit2 ne lit pas un FS SSH/WSL directement. Décision : **fallback git CLI** (`RemoteGitRepository`) côté distant via `ProcessSpawner`. À valider (perf, parsing). L9.
|
||||||
|
5. **Synchro temps réel UI ↔ PTY** : volume d'octets élevé ; backpressure des Channels Tauri, throttling/coalescing côté front. **Spike** L3.
|
||||||
|
6. ~~**Injection `conventionFile`** : symlink vs copie du `.md` vers `CLAUDE.md`/`AGENTS.md` ; conflits si fichier existant, .gitignore, droits Windows (symlinks).~~ **Résolu (§14.1)** : cwd isolé par agent dans `.ideai/run/<id>/` — plus de conflit à la racine, convention file généré par copie simple.
|
||||||
|
7. **SSH auth** : agent/clé/mot de passe/known_hosts ; choix russh (rustls) vs ssh2 (libssh2/OpenSSL — impacte point 2). Décision à figer début L9.
|
||||||
|
8. **WSL chemins** : conversion `/mnt/c/...` ↔ `\\wsl$\...`, distros multiples, perf I/O cross-boundary. Spike L9.
|
||||||
|
9. **Détection d'édition hors-app** des `.md`/templates (content hash) et résolution de conflit lors du sync. L7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document maintenu par l'Agent Architecture — base du jalon « cadrage architecture » avant tout code applicatif.*
|
||||||
187
CLAUDE.md
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# IdeA — Contexte & Méthode de travail
|
||||||
|
|
||||||
|
> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA.
|
||||||
|
> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Mon rôle : chef d'orchestre, pas développeur
|
||||||
|
|
||||||
|
Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail.
|
||||||
|
Je suis responsable de :
|
||||||
|
|
||||||
|
- Découper le travail en tâches claires et autonomes.
|
||||||
|
- Attribuer chaque tâche aux bons agents.
|
||||||
|
- Garantir que le cycle de développement/test est respecté.
|
||||||
|
- Faire respecter les principes d'architecture (SOLID, Hexagonal).
|
||||||
|
- Maintenir la cohérence globale du projet et de ce document.
|
||||||
|
- Arbitrer et valider avant toute action irréversible ou sortante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Les agents
|
||||||
|
|
||||||
|
### 2.1 Agent Architecture (1 pour tout le projet)
|
||||||
|
- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**.
|
||||||
|
- Définit les frontières (domaine / application / infrastructure), les ports, les contrats.
|
||||||
|
- Valide que chaque nouvelle feature respecte la structure avant son développement.
|
||||||
|
- Tient à jour la cartographie d'architecture et les conventions.
|
||||||
|
|
||||||
|
### 2.2 Agents de Développement
|
||||||
|
- Écrivent le code des features.
|
||||||
|
- Respectent strictement l'architecture définie par l'agent Architecture.
|
||||||
|
- Code **propre, structuré, stable**.
|
||||||
|
- Reçoivent les rapports d'erreurs des agents de test et corrigent.
|
||||||
|
|
||||||
|
### 2.3 Agents de Test
|
||||||
|
- **Chaque agent de développement est appairé avec un agent de test dédié.**
|
||||||
|
- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées.
|
||||||
|
- Produisent un **rapport d'erreurs** clair quand un test échoue.
|
||||||
|
- Re-testent après chaque correction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Le cycle de développement (boucle obligatoire)
|
||||||
|
|
||||||
|
Pour **chaque** feature implémentée ou modifiée :
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Agent Architecture → valide le découpage et les contrats (ports/interfaces)
|
||||||
|
2. Agent Développement → écrit le code
|
||||||
|
3. Agent Test → écrit les tests unitaires + les exécute
|
||||||
|
4a. Tests OK → feature validée, on passe à la suite
|
||||||
|
4b. Tests KO → rapport d'erreurs → retour à l'agent Développement
|
||||||
|
→ correction → retour à l'étape 3 (boucle jusqu'au vert)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas.
|
||||||
|
Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Principes de code
|
||||||
|
|
||||||
|
- **SOLID** appliqué au maximum.
|
||||||
|
- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...).
|
||||||
|
- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe.
|
||||||
|
- Tests unitaires systématiques ; couverture des features critiques.
|
||||||
|
- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Vision produit : IdeA
|
||||||
|
|
||||||
|
**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.**
|
||||||
|
|
||||||
|
### Fonctionnalités clés
|
||||||
|
- **Multi-projets en parallèle** : un **onglet par projet**.
|
||||||
|
- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement.
|
||||||
|
- **Agents par projet** : chaque projet a ses propres agents.
|
||||||
|
- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets.
|
||||||
|
- **Création d'agents** : depuis zéro ou à partir d'un template.
|
||||||
|
- **Synchronisation template → agents** : option « garder l'agent à jour ».
|
||||||
|
Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour.
|
||||||
|
- **Contextes d'agents stockés en `.md`** (toujours).
|
||||||
|
- **Création de projet** = définition de son **project root**.
|
||||||
|
|
||||||
|
### Intégrations
|
||||||
|
- **Git** intégré.
|
||||||
|
- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH.
|
||||||
|
- **Développement WSL** : travailler sur une WSL depuis Windows.
|
||||||
|
|
||||||
|
### Plateformes & livraison
|
||||||
|
- Cible : **macOS, Linux, Windows**.
|
||||||
|
- Première phase de compilation : **Linux et Windows**.
|
||||||
|
- Livraison :
|
||||||
|
- **Windows** : `setup.exe`.
|
||||||
|
- **Linux** : **AppImage** (doit fonctionner sur les différentes distributions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Stack technique (validée)
|
||||||
|
|
||||||
|
- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs).
|
||||||
|
- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations).
|
||||||
|
- **Frontend / UI** : **TypeScript + React**.
|
||||||
|
- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust).
|
||||||
|
- **Git** : **libgit2** via `git2` (Rust).
|
||||||
|
- **SSH** : `russh` / `ssh2` (Rust).
|
||||||
|
- **WSL** : invocation de `wsl.exe` depuis le backend.
|
||||||
|
|
||||||
|
## 7. Layout des terminaux (exigence produit)
|
||||||
|
|
||||||
|
Disposition en **grille redimensionnable de type tableur (Excel)** :
|
||||||
|
|
||||||
|
- Splits redimensionnables horizontaux **et** verticaux.
|
||||||
|
- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone.
|
||||||
|
- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur.
|
||||||
|
- Chaque cellule de la grille héberge un terminal.
|
||||||
|
- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture.
|
||||||
|
|
||||||
|
## 8. Stockage des contextes & liaison aux templates
|
||||||
|
|
||||||
|
- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet).
|
||||||
|
- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root.
|
||||||
|
*(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)*
|
||||||
|
- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet :
|
||||||
|
- le `.md` de l'agent,
|
||||||
|
- le template d'origine (le cas échéant),
|
||||||
|
- `synchronized: true/false`,
|
||||||
|
- la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible).
|
||||||
|
- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ.
|
||||||
|
|
||||||
|
## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`)
|
||||||
|
|
||||||
|
Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA :
|
||||||
|
|
||||||
|
1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`).
|
||||||
|
2. **Stratégie d'injection du contexte `.md`** :
|
||||||
|
- `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…).
|
||||||
|
- `flag` : passer le chemin via un argument.
|
||||||
|
- `stdin` : piper le contenu.
|
||||||
|
- `env` : passer via variable d'environnement.
|
||||||
|
|
||||||
|
Exemple de profil :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "claude-code",
|
||||||
|
"name": "Claude Code",
|
||||||
|
"command": "claude",
|
||||||
|
"args": [],
|
||||||
|
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||||
|
"detect": "claude --version",
|
||||||
|
"cwd": "{projectRoot}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message).
|
||||||
|
|
||||||
|
**Règles produit :**
|
||||||
|
- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut.
|
||||||
|
- Les commandes des profils sont **pré-remplies mais éditables**.
|
||||||
|
- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA.
|
||||||
|
|
||||||
|
**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil.
|
||||||
|
|
||||||
|
## 10. Fenêtres & onglets
|
||||||
|
|
||||||
|
- **Par défaut : un onglet par projet** (comme les IDE classiques).
|
||||||
|
- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet.
|
||||||
|
- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets.
|
||||||
|
|
||||||
|
## 11. Feuille de route
|
||||||
|
|
||||||
|
1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**.
|
||||||
|
2. Puis MVP incrémental selon le cycle dev/test de la section 3.
|
||||||
|
|
||||||
|
## 12. Autonomie d'exécution dans le projet
|
||||||
|
|
||||||
|
L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**.
|
||||||
|
|
||||||
|
- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**.
|
||||||
|
- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…).
|
||||||
|
- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 2026-06-05*
|
||||||
187
CONTEXT.md
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# IdeA — Contexte & Méthode de travail
|
||||||
|
|
||||||
|
> Ce document définit **mon rôle**, **la méthode de développement** et **la vision produit** du projet IdeA.
|
||||||
|
> Il fait autorité sur la façon dont le projet est piloté. Toute évolution de méthode doit être répercutée ici.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Mon rôle : chef d'orchestre, pas développeur
|
||||||
|
|
||||||
|
Je **n'écris pas de code moi-même**. Mon rôle est de **piloter des agents** qui réalisent le travail.
|
||||||
|
Je suis responsable de :
|
||||||
|
|
||||||
|
- Découper le travail en tâches claires et autonomes.
|
||||||
|
- Attribuer chaque tâche aux bons agents.
|
||||||
|
- Garantir que le cycle de développement/test est respecté.
|
||||||
|
- Faire respecter les principes d'architecture (SOLID, Hexagonal).
|
||||||
|
- Maintenir la cohérence globale du projet et de ce document.
|
||||||
|
- Arbitrer et valider avant toute action irréversible ou sortante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Les agents
|
||||||
|
|
||||||
|
### 2.1 Agent Architecture (1 pour tout le projet)
|
||||||
|
- Garant de l'architecture globale : **Hexagonale (Ports & Adapters)** et principes **SOLID**.
|
||||||
|
- Définit les frontières (domaine / application / infrastructure), les ports, les contrats.
|
||||||
|
- Valide que chaque nouvelle feature respecte la structure avant son développement.
|
||||||
|
- Tient à jour la cartographie d'architecture et les conventions.
|
||||||
|
|
||||||
|
### 2.2 Agents de Développement
|
||||||
|
- Écrivent le code des features.
|
||||||
|
- Respectent strictement l'architecture définie par l'agent Architecture.
|
||||||
|
- Code **propre, structuré, stable**.
|
||||||
|
- Reçoivent les rapports d'erreurs des agents de test et corrigent.
|
||||||
|
|
||||||
|
### 2.3 Agents de Test
|
||||||
|
- **Chaque agent de développement est appairé avec un agent de test dédié.**
|
||||||
|
- Écrivent et exécutent les **tests unitaires** des features implémentées ou modifiées.
|
||||||
|
- Produisent un **rapport d'erreurs** clair quand un test échoue.
|
||||||
|
- Re-testent après chaque correction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Le cycle de développement (boucle obligatoire)
|
||||||
|
|
||||||
|
Pour **chaque** feature implémentée ou modifiée :
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Agent Architecture → valide le découpage et les contrats (ports/interfaces)
|
||||||
|
2. Agent Développement → écrit le code
|
||||||
|
3. Agent Test → écrit les tests unitaires + les exécute
|
||||||
|
4a. Tests OK → feature validée, on passe à la suite
|
||||||
|
4b. Tests KO → rapport d'erreurs → retour à l'agent Développement
|
||||||
|
→ correction → retour à l'étape 3 (boucle jusqu'au vert)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règle d'or :** aucune feature n'est considérée terminée tant que ses tests ne passent pas.
|
||||||
|
Je relaie fidèlement les résultats : si des tests échouent, je le dis avec la sortie réelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Principes de code
|
||||||
|
|
||||||
|
- **SOLID** appliqué au maximum.
|
||||||
|
- **Architecture Hexagonale** (Ports & Adapters) : le domaine métier est isolé des détails techniques (UI, terminal, git, SSH, système de fichiers...).
|
||||||
|
- Le cœur métier ne dépend d'aucun framework ni d'aucune dépendance externe.
|
||||||
|
- Tests unitaires systématiques ; couverture des features critiques.
|
||||||
|
- Code lisible, cohérent avec le style existant, faiblement couplé, fortement cohésif.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Vision produit : IdeA
|
||||||
|
|
||||||
|
**IdeA est un IDE next-gen 100 % IA.** On n'y code pas : **on gère des IA.**
|
||||||
|
|
||||||
|
### Fonctionnalités clés
|
||||||
|
- **Multi-projets en parallèle** : un **onglet par projet**.
|
||||||
|
- **Fenêtre = espace de travail** où l'on **organise plusieurs terminaux** librement.
|
||||||
|
- **Agents par projet** : chaque projet a ses propres agents.
|
||||||
|
- **Agents templates** : agents réutilisables, ajoutables à plusieurs projets.
|
||||||
|
- **Création d'agents** : depuis zéro ou à partir d'un template.
|
||||||
|
- **Synchronisation template → agents** : option « garder l'agent à jour ».
|
||||||
|
Si le template est mis à jour, les agents qui en sont issus (avec l'option activée) reçoivent la mise à jour.
|
||||||
|
- **Contextes d'agents stockés en `.md`** (toujours).
|
||||||
|
- **Création de projet** = définition de son **project root**.
|
||||||
|
|
||||||
|
### Intégrations
|
||||||
|
- **Git** intégré.
|
||||||
|
- **Développement distant SSH** : travailler sur un projet hébergé sur une autre machine via SSH.
|
||||||
|
- **Développement WSL** : travailler sur une WSL depuis Windows.
|
||||||
|
|
||||||
|
### Plateformes & livraison
|
||||||
|
- Cible : **macOS, Linux, Windows**.
|
||||||
|
- Première phase de compilation : **Linux et Windows**.
|
||||||
|
- Livraison :
|
||||||
|
- **Windows** : `setup.exe`.
|
||||||
|
- **Linux** : **AppImage** (doit fonctionner sur les différentes distributions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Stack technique (validée)
|
||||||
|
|
||||||
|
- **Shell applicatif** : **Tauri v2** (binaires légers, performants, multi-OS, AppImage + installeur `setup.exe`/NSIS Windows natifs).
|
||||||
|
- **Cœur / backend** : **Rust** — stabilité, performance, et expression idiomatique du domaine hexagonal (ports = traits, adapters = implémentations).
|
||||||
|
- **Frontend / UI** : **TypeScript + React**.
|
||||||
|
- **Terminaux** : **xterm.js** (rendu) + **portable-pty** (PTY côté Rust).
|
||||||
|
- **Git** : **libgit2** via `git2` (Rust).
|
||||||
|
- **SSH** : `russh` / `ssh2` (Rust).
|
||||||
|
- **WSL** : invocation de `wsl.exe` depuis le backend.
|
||||||
|
|
||||||
|
## 7. Layout des terminaux (exigence produit)
|
||||||
|
|
||||||
|
Disposition en **grille redimensionnable de type tableur (Excel)** :
|
||||||
|
|
||||||
|
- Splits redimensionnables horizontaux **et** verticaux.
|
||||||
|
- L'utilisateur peut **définir le nombre de colonnes dans une ligne** et **le nombre de lignes dans une colonne**, indépendamment par zone.
|
||||||
|
- Possibilité de **fusionner des cellules** (ex. fusionner deux colonnes sur une ligne), à la manière des cellules fusionnées d'un tableur.
|
||||||
|
- Chaque cellule de la grille héberge un terminal.
|
||||||
|
- → Modèle de layout récursif/imbriqué (pas une grille rigide uniforme) à concevoir par l'agent Architecture.
|
||||||
|
|
||||||
|
## 8. Stockage des contextes & liaison aux templates
|
||||||
|
|
||||||
|
- **Templates d'agents** : stockés dans l'**IDE** (dossier de données utilisateur global de l'app, hors projet).
|
||||||
|
- **Agents de projet** : leurs `.md` sont stockés dans un dossier **`.ideai/`** à la racine du project root.
|
||||||
|
*(Nom choisi pour éviter toute collision avec le `.idea` de JetBrains.)*
|
||||||
|
- **Manifeste de liaison** dans `.ideai/` (ex. `.ideai/agents.json`) qui mappe pour chaque agent de projet :
|
||||||
|
- le `.md` de l'agent,
|
||||||
|
- le template d'origine (le cas échéant),
|
||||||
|
- `synchronized: true/false`,
|
||||||
|
- la **version du template** au dernier sync (pour détecter qu'une mise à jour est disponible).
|
||||||
|
- **Synchro template → agents** : quand un template est mis à jour, les agents liés avec `synchronized: true` reçoivent la MAJ.
|
||||||
|
|
||||||
|
## 9. Moteur IA : adaptateur de CLI flexible (Port `AgentRuntime`)
|
||||||
|
|
||||||
|
Chaque IA est décrite par un **profil déclaratif** (config éditable, pas du code), implémentation d'un **Port** `AgentRuntime` côté domaine. Deux variables clés par IA :
|
||||||
|
|
||||||
|
1. **Commande de lancement** + arguments (ex. `claude`, `codex`, `gemini`, `aider`).
|
||||||
|
2. **Stratégie d'injection du contexte `.md`** :
|
||||||
|
- `conventionFile` : écrire/symlink le `.md` vers le fichier attendu par la CLI (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`…).
|
||||||
|
- `flag` : passer le chemin via un argument.
|
||||||
|
- `stdin` : piper le contenu.
|
||||||
|
- `env` : passer via variable d'environnement.
|
||||||
|
|
||||||
|
Exemple de profil :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "claude-code",
|
||||||
|
"name": "Claude Code",
|
||||||
|
"command": "claude",
|
||||||
|
"args": [],
|
||||||
|
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||||
|
"detect": "claude --version",
|
||||||
|
"cwd": "{projectRoot}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Profils intégrés (références) :** Claude Code (`claude` → `CLAUDE.md`), OpenAI Codex CLI (`codex` → `AGENTS.md`), Gemini CLI (`gemini` → `GEMINI.md`), Aider (`aider` → args/message).
|
||||||
|
|
||||||
|
**Règles produit :**
|
||||||
|
- **Premier lancement de l'IDE** : un assistant (first-run) **demande à l'utilisateur** quels profils d'IA configurer. On ne présume rien par défaut.
|
||||||
|
- Les commandes des profils sont **pré-remplies mais éditables**.
|
||||||
|
- L'utilisateur peut **ajouter sa propre commande CLI** (profil custom) pour n'importe quelle IA.
|
||||||
|
|
||||||
|
**Lancement d'un agent :** à l'**activation de l'agent**, on ouvre une cellule terminal (PTY) avec le bon `cwd`, on injecte le contexte `.md`, et on **auto-lance** la CLI du profil.
|
||||||
|
|
||||||
|
## 10. Fenêtres & onglets
|
||||||
|
|
||||||
|
- **Par défaut : un onglet par projet** (comme les IDE classiques).
|
||||||
|
- **Drag & drop d'un onglet** hors de la fenêtre → **crée une nouvelle fenêtre OS** portant ce projet.
|
||||||
|
- **Multi-fenêtres OS supporté** ; chaque fenêtre possède un ou plusieurs onglets/projets.
|
||||||
|
|
||||||
|
## 11. Feuille de route
|
||||||
|
|
||||||
|
1. **Cadrage architecture complet d'abord** (jalon en cours) : l'agent Architecture produit la cartographie complète — domaine, ports, adapters, modules, arborescence — **avant tout code**.
|
||||||
|
2. Puis MVP incrémental selon le cycle dev/test de la section 3.
|
||||||
|
|
||||||
|
## 12. Autonomie d'exécution dans le projet
|
||||||
|
|
||||||
|
L'utilisateur m'accorde un **accès large et autonome** sur le dossier du projet : je peux lire, créer, modifier des fichiers et exécuter les commandes de développement (cargo, npm, npx, git, etc.) **sans demander confirmation à chaque fois**.
|
||||||
|
|
||||||
|
- Concrètement, ces autorisations sont matérialisées dans `.claude/settings.local.json` (mode `acceptEdits` + `Bash`/`Read`/`Edit`/`Write` autorisés), pas dans ce document — CONTEXT.md ne fait que **documenter l'intention**.
|
||||||
|
- **Garde-fous conservés** : les actions destructrices ou hors-projet restent bloquées (`sudo`, `rm -rf` sur `/`/`~`/`$HOME`, `mkfs`, `dd`, `shutdown`/`reboot`…).
|
||||||
|
- L'esprit du rôle (§1) ne change pas : je reste **chef d'orchestre**. L'autonomie porte sur l'exécution mécanique, pas sur l'arbitrage des décisions produit/archi, ni sur les **actions sortantes** (push, publication) qui restent soumises à validation explicite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 2026-06-05*
|
||||||
4975
Cargo.lock
generated
Normal file
35
Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"crates/domain",
|
||||||
|
"crates/application",
|
||||||
|
"crates/infrastructure",
|
||||||
|
"crates/app-tauri",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
uuid = { version = "1", features = ["serde", "v4", "v5", "macro-diagnostics"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
thiserror = "2"
|
||||||
|
async-trait = "0.1"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "fs", "io-util"] }
|
||||||
|
# Local git via libgit2. Network features (https/ssh → openssl) are off for L8:
|
||||||
|
# only local operations (status/commit/branch/checkout/log) are in scope; remote
|
||||||
|
# push/pull and static vendoring for the AppImage are deferred to L9/L11.
|
||||||
|
git2 = { version = "0.20", default-features = false }
|
||||||
|
|
||||||
|
# Internal crates
|
||||||
|
domain = { path = "crates/domain" }
|
||||||
|
application = { path = "crates/application" }
|
||||||
|
infrastructure = { path = "crates/infrastructure" }
|
||||||
|
|
||||||
|
# Tauri v2
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
54
agents-dev/L0-core-domain.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# L0 — Socle domaine & ports
|
||||||
|
|
||||||
|
**Binôme :** `dev-core-domain` / `test-core-domain`
|
||||||
|
**Crate :** `crates/domain` (pur, zéro I/O)
|
||||||
|
**Dépendances amont :** aucune (fondation du projet).
|
||||||
|
**Statut :** en cours.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Poser le **cœur hexagonal** : toutes les entités, value objects, invariants, **ports (traits)**, domain events et la **logique de layout pure**. Aucun adapter, aucune I/O.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
|
||||||
|
### Entities & Value Objects (cf. ARCHITECTURE §3)
|
||||||
|
- IDs typés (`ProjectId`, `AgentId`, `TemplateId`, `ProfileId`, `SessionId`, `WindowId`, `TabId`, `NodeId`).
|
||||||
|
- `Project`, `ProjectPath`, `Agent`, `AgentOrigin`, `AgentTemplate`, `TemplateVersion`, `AgentProfile`, `ContextInjection`.
|
||||||
|
- `TerminalSession`, `SessionKind`, `PtySize`, `RemoteRef`/`SshAuth`, `GitRepository`, `AgentManifest`/`ManifestEntry`, `Workspace`/`Window`/`Tab`.
|
||||||
|
- `MarkdownDoc` (VO contenu .md).
|
||||||
|
- Tous les **invariants** documentés en §3 doivent être appliqués (constructeurs validants / `try_new`).
|
||||||
|
|
||||||
|
### Logique de layout pure (cf. ARCHITECTURE §7)
|
||||||
|
- `LayoutNode` (`Leaf`/`Split`/`Grid`), `SplitContainer`, `WeightedChild`, `GridContainer`, `GridCell`.
|
||||||
|
- Opérations **pures** : `split`, `merge`, `resize`, `move` → `LayoutTree -> Result<LayoutTree, LayoutError>`.
|
||||||
|
- Validation des invariants (poids > 0, pas de chevauchement de spans, surface couverte, 1 session par leaf max).
|
||||||
|
|
||||||
|
### Ports (traits) — définitions seulement, pas d'impl
|
||||||
|
`AgentRuntime`, `PtyPort`, `RemoteHost`, `ProcessSpawner`, `FileSystem`, `TemplateStore`, `ProjectStore`, `AgentContextStore`, `GitRepository`, `EventBus`, `Clock`, `IdGenerator` (signatures conceptuelles en ARCHITECTURE §4).
|
||||||
|
|
||||||
|
### Domain events & erreurs
|
||||||
|
- `DomainEvent` (enum complet de §3.2).
|
||||||
|
- Types d'erreur par domaine (`LayoutError`, et erreurs de port définies ici si partagées).
|
||||||
|
|
||||||
|
### serde
|
||||||
|
- Autorisé **uniquement** pour les types persistés (manifeste, layout, profils) — dérive `Serialize`/`Deserialize`. Aucune autre dépendance I/O.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
|
||||||
|
- Invariants d'entités : rejets attendus (chemin relatif, `synchronized` sans template, port SSH hors plage, etc.).
|
||||||
|
- **Layout** (cœur du lot) : `split`/`merge`/`resize`/`move` — cas nominaux + cas d'erreur (chevauchement, poids ≤ 0, span hors grille, session dupliquée).
|
||||||
|
- Déterminisme via `FixedClock`/`SeqIdGenerator`.
|
||||||
|
- `ContextInjection` : validation des 4 variantes (target relatif, var env valide, flag non vide).
|
||||||
|
- Sérialisation round-trip JSON des types persistés.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- `cargo test -p domain` vert.
|
||||||
|
- `crates/domain/Cargo.toml` ne dépend d'aucun crate I/O (vérifié).
|
||||||
|
- Tous les ports compilent et sont documentés.
|
||||||
|
- Logique de layout couverte (cas limites inclus).
|
||||||
|
|
||||||
|
## Notes / points d'attention
|
||||||
|
|
||||||
|
- Choisir `async_trait` vs `-> impl Future` pour les ports I/O (à figer ici, impacte tous les lots).
|
||||||
|
- Garder les traits **fins** (Interface Segregation) : ne pas fusionner FS/PTY/Process.
|
||||||
44
agents-dev/L1-ipc-bridge.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# L1 — Composition root & IPC
|
||||||
|
|
||||||
|
**Binôme :** `dev-ipc-bridge` / `test-ipc-bridge`
|
||||||
|
**Zones :** `crates/app-tauri`, `frontend/ports`, `frontend/adapters`
|
||||||
|
**Dépendances amont :** L0 (ports figés).
|
||||||
|
**Statut :** suivant (enchaîné après L0).
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Mettre en place le **squelette Tauri qui tourne** : composition root (DI), pont IPC bidirectionnel, et la couche ports/adapters du frontend (avec mocks) — pour que le front soit développable **sans backend** dès les lots suivants.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
|
||||||
|
### Backend `app-tauri` (driving adapter + composition root)
|
||||||
|
- **Composition root** : instancier les adapters concrets (au départ : `LocalFileSystem`, `TokioBroadcastEventBus`, `SystemClock`, `UuidGenerator`) et les injecter dans les use cases via `tauri::State` (`Arc<dyn Port>`).
|
||||||
|
- Registre des `#[tauri::command]` (squelette, mapping DTO ↔ use case) et `ErrorDTO`.
|
||||||
|
- **`TauriEventRelay`** : souscrit l'`EventBus` domaine → relaie en events/Channels Tauri.
|
||||||
|
- **Bridge PTY ↔ Tauri Channel** (`tauri::ipc::Channel`) : infrastructure générique de flux d'octets par session (sera consommée par L3).
|
||||||
|
- App Tauri v2 minimale qui démarre (fenêtre vide).
|
||||||
|
|
||||||
|
### Frontend (hexagonal côté UI)
|
||||||
|
- `frontend/ports` : gateways TS (`AgentGateway`, `TerminalGateway`, `ProjectGateway`, `LayoutGateway`, `GitGateway`, `RemoteGateway`).
|
||||||
|
- `frontend/adapters` : impl via `@tauri-apps/api` (`invoke`/`listen`/`Channel`).
|
||||||
|
- `frontend/adapters/mock` : impl mock de chaque gateway.
|
||||||
|
- `frontend/app` : bootstrap React + Vite, provider de DI des adapters (réel vs mock).
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
|
||||||
|
- Backend : mapping commands ↔ use cases (un use case in-memory simple validant le wiring).
|
||||||
|
- Relais `EventBus` → events Tauri (un `DomainEvent` publié arrive bien côté relais).
|
||||||
|
- Front : chaque gateway mock satisfait l'interface du port (typecheck + tests Vitest).
|
||||||
|
- Provider de DI : bascule réel/mock fonctionnelle.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- L'app Tauri démarre (fenêtre vide) sur Linux.
|
||||||
|
- `cargo test -p app-tauri` et `vitest` verts.
|
||||||
|
- Front compile et tourne en mode **mock** sans backend.
|
||||||
|
- Aucun `invoke()` direct dans les composants (uniquement via gateways).
|
||||||
|
|
||||||
|
## Points d'attention / spikes
|
||||||
|
|
||||||
|
- Forme du bridge PTY↔Channel (backpressure) — préparé ici, stressé en L3.
|
||||||
|
- Convention de (dé)sérialisation DTO Rust ↔ TS (serde camelCase ?). À figer ici.
|
||||||
38
agents-dev/L10-windows.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# L10 — Fenêtres & multi-window
|
||||||
|
|
||||||
|
**Binôme :** `dev-windows` / `test-windows`
|
||||||
|
**Zones :** `application`, `app-tauri`, `frontend/app`
|
||||||
|
**Dépendances amont :** L0, L1, L2, L4.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Gestion des fenêtres et onglets : un onglet par projet ; **drag d'un onglet hors de la fenêtre → nouvelle fenêtre OS** portant ce projet.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Entités `Workspace`/`Window`/`Tab` + persistance (`workspace.json`, machine-local).
|
||||||
|
- Use case `MoveTabToNewWindow` (réaffectation `WindowId`, l'onglet est déplacé, pas dupliqué).
|
||||||
|
- `app-tauri` : création de `WebviewWindow`, transfert d'état, fermeture de l'onglet source.
|
||||||
|
- Front : barre d'onglets, drag & drop, restauration de session.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- `MoveTabToNewWindow` : invariants (un projet dans un seul onglet à la fois ; fenêtre ≥ 1 onglet ou fermée).
|
||||||
|
- Persistance workspace round-trip.
|
||||||
|
- Front : interactions onglets (mock).
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` + `vitest` verts ; détacher un onglet en nouvelle fenêtre fonctionne (dev manuel).
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
### ✅ Backend (vert)
|
||||||
|
- **Domaine** : opération **pure** `Workspace::move_tab_to_new_window(tab, new_window)` (`layout.rs`) — l'onglet est *déplacé* (jamais dupliqué) ; fenêtre source vidée → supprimée ; onglet actif déplacé → repli sur un onglet restant. Variante d'erreur `LayoutError::TabNotFound`. 4 tests domaine.
|
||||||
|
- **Application** (`application/window/`) : `MoveTabToNewWindow` (charge le workspace, mint `WindowId`, applique l'op pure, persiste via `ProjectStore`). 2 tests (store mock). La persistance round-trip du workspace est déjà couverte par `FsProjectStore` (L2).
|
||||||
|
- `cargo test --workspace` : **323 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ IPC `app-tauri` (vert)
|
||||||
|
- Composition root : use case `MoveTabToNewWindow` injecté. Commande `move_tab_to_new_window(tabId)` : applique la topologie (persistée) **et ouvre une vraie `WebviewWindow`** (primitive de détach résolue). DTO `MoveTabResultDto` + `parse_tab_id`. Test `tests/dto_window.rs` (2). Workspace **325 verts, 0 régression**, clippy clean.
|
||||||
|
|
||||||
|
### ⏳ Reste (fait pendant la refonte disposition L11)
|
||||||
|
- **Front multi-fenêtres** : adopter le modèle `Workspace`/`Window`/`Tab` persistant (aujourd'hui les onglets vivent en state React transitoire), barre d'onglets, **DnD detach** + handoff d'état vers la nouvelle fenêtre. Couplé à la refonte de disposition IDE (L11), donc traité là-bas pour éviter de construire une barre d'onglets jetable.
|
||||||
|
|
||||||
|
## Spike (cf. ARCHITECTURE §13)
|
||||||
|
- DnD inter-fenêtres Tauri (le DnD HTML ne traverse pas les fenêtres OS) → protocole « detach » via store + event.
|
||||||
45
agents-dev/L11-packaging.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# L11 — Packaging & livraison
|
||||||
|
|
||||||
|
**Binôme :** `dev-packaging` / `test-packaging`
|
||||||
|
**Zones :** `app-tauri` (bundle), CI
|
||||||
|
**Dépendances amont :** transverse (mûrit avec les autres lots) ; finalisé en fin de cycle.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Livrer IdeA : **`setup.exe` (NSIS) Windows** et **AppImage Linux multi-distro**. macOS plus tard.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Config bundle Tauri v2 : NSIS (Windows), AppImage (Linux).
|
||||||
|
- Vendoring statique des deps natives (git2/openssl → préférer `rustls` pour russh ; features git2) pour la portabilité AppImage.
|
||||||
|
- Pipeline CI : build Linux + Windows, artefacts publiés.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Le bundle se construit sur Linux et Windows (CI verte).
|
||||||
|
- L'AppImage **démarre sur ≥3 distros** (Ubuntu, Fedora, Arch) — smoke test.
|
||||||
|
- L'installeur Windows installe/lance/désinstalle proprement.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- CI produit un `setup.exe` et un AppImage fonctionnels ; smoke tests multi-distro verts.
|
||||||
|
|
||||||
|
## Spike (cf. ARCHITECTURE §13)
|
||||||
|
- AppImage multi-distro : glibc/openssl/libgit2 liés dynamiquement = risque ; valider le vendoring statique tôt (coordonné avec L8/L9).
|
||||||
|
|
||||||
|
## Notes de build vérifiées (2026-06-04, premier build sur Arch)
|
||||||
|
- **CLI** : `@tauri-apps/cli` v2 installé en devDependency frontend ; binaire à `frontend/node_modules/.bin/tauri`.
|
||||||
|
- **Hooks `tauri.conf.json`** : Tauri exécute `beforeBuildCommand`/`beforeDevCommand` avec cwd = `IdeA/crates/`. Les chemins npm doivent donc être `--prefix ../frontend` (et NON `../../frontend` ni `frontend`).
|
||||||
|
- **Arch Linux — `linuxdeploy` strip échoue** : le `strip` embarqué dans `linuxdeploy-x86_64.AppImage` ne comprend pas la section ELF `.relr.dyn` des libs Arch modernes (`libzstd.so.1`, `libyuv.so`…) → erreur `unknown type [0x13] section .relr.dyn`. **Parade** : exporter **`NO_STRIP=true`** avant `tauri build`. À intégrer dans la CI/scripts de build Linux.
|
||||||
|
- Build de référence OK : `cd crates/app-tauri && NO_STRIP=true tauri build --bundles appimage` → `target/release/bundle/appimage/IdeA_0.1.0_amd64.AppImage` (~101 Mo).
|
||||||
|
- Le 1er échec `failed to run linuxdeploy` masquait le vrai message (strip) ; `--verbose` est nécessaire pour le voir.
|
||||||
|
- **Écran blanc au lancement (Linux/WebKitGTK)** : le renderer DMABUF de WebKitGTK rend une fenêtre blanche sur beaucoup de configs Linux récentes (Mesa/Nvidia, fréquent sur Arch). **Fix baké dans `crates/app-tauri/src/main.rs`** : on positionne `WEBKIT_DISABLE_DMABUF_RENDERER=1` au début de `main()` (cfg `target_os = "linux"`, seulement si non déjà défini) avant l'init du webview. Plus besoin de variable d'env côté utilisateur. Vérifié visuellement OK sur Arch (UI projets + health-check rendus).
|
||||||
|
|
||||||
|
## Avancement (2026-06-05)
|
||||||
|
|
||||||
|
### ✅ Fait
|
||||||
|
- **Icônes** générées (`tauri icon` → `crates/app-tauri/icons/`, monogramme « IA » sombre/accent) — manquaient, requises par le bundle.
|
||||||
|
- **AppImage Linux reconstruite** : `target/release/bundle/appimage/IdeA_0.1.0_amd64.AppImage` (~103 Mo), validée (ELF AppImage-runtime, libfuse2 présent, se lance directement). `git2` lié à la **libgit2 système** via `.cargo/config.toml` (`LIBGIT2_SYS_USE_PKG_CONFIG=1`).
|
||||||
|
- **`beforeBuildCommand`/`beforeDevCommand`** confirmés à `--prefix ../frontend` (cwd des hooks = `IdeA/crates/`).
|
||||||
|
- **Refonte disposition IDE** (passe UI/altitude) ✅ : `ProjectsView` réécrit en disposition d'IDE — **barre d'onglets projets** en haut, **sidebar** (onglets Projects/Agents/Templates/Git, un panneau à la fois) + **zone principale** = `LayoutGrid` (grille de terminaux) qui remplit la hauteur. App shell en `h-full`. Composants `ProjectLauncher`/`ProjectTabs`/`Workspace` extraits. Hooks de test préservés ; front **158 tests verts**, `tsc` clean. (Note : `beforeBuildCommand` retiré de `tauri.conf.json` — chemins de hook ambigus selon le cwd d'invocation ; on **build le front manuellement** (`npm --prefix frontend run build`) avant `tauri build` lancé **depuis la racine du repo**. C'est le recette déterministe vérifiée.)
|
||||||
|
|
||||||
|
### ⏳ Reste
|
||||||
|
- **Vendoring statique** pour AppImage portable multi-distro : passer git2 en `vendored-libgit2` (nécessite **cmake**, absent de la machine actuelle) + `rustls` pour russh (L9). Aujourd'hui l'AppImage lie la libgit2 **système** → portable seulement vers des distros fournissant libgit2 ≥ 1.9.
|
||||||
|
- **Windows `setup.exe` (NSIS)** : non constructible ici (Linux) → CI Windows.
|
||||||
|
- **CI** Linux+Windows (avec `NO_STRIP=true` côté Linux) + smoke tests ≥3 distros.
|
||||||
65
agents-dev/L12-skills.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# L12 — Skills
|
||||||
|
|
||||||
|
**Binôme :** `dev-skills` / `test-skills`
|
||||||
|
**Zones :** `domain/skill`, `application/skill`, `infrastructure/store`, `frontend/features/skills`
|
||||||
|
**Dépendances amont :** L0, L1, L5, L6 (convention file généré à l'activation), L7 (store global réutilisé).
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Modéliser les **Skills** : workflows réutilisables (équivalent universel des slash-commands, sans dépendance à la syntaxe `/command` d'un modèle). Stockage global IDE + projet, assignation agent↔skills, **injection des skills assignés dans le convention file** généré à l'activation de l'agent. Cf. ARCHITECTURE §14.2.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- **Domaine** : entité `Skill { id, name, content_md: MarkdownDoc, scope: SkillScope(Global|Project) }`. Invariants : `name` non vide, `content_md` non vide. Event `SkillAssigned`.
|
||||||
|
- **Port `SkillStore`** : CRUD skills globaux (`<app_data>/IdeA/skills/`) + skills projet (`.ideai/skills/<name>.md`), résolution selon `scope` (compose `FileSystem`/store global comme L7).
|
||||||
|
- **AgentManifest** : étendre pour porter la liste `skills: Vec<SkillRef>` assignés à chaque agent (0..N).
|
||||||
|
- **Use cases** (`application/skill`) : `CreateSkill`, `UpdateSkill`, `DeleteSkill`, `ListSkills(scope)`, `AssignSkillToAgent`, `UnassignSkillFromAgent`.
|
||||||
|
- **Injection** : à l'activation (fil L6), composer le convention file en concaténant persona agent + chemin project root + **skills assignés** (lus via `SkillStore`). Pas de mécanisme CLI propriétaire.
|
||||||
|
- **Front** : onglet/section Skills (liste globale + projet, CRUD, éditeur md), assignation skills↔agent dans `AgentsPanel`.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- `Skill` rejette `name`/`content_md` vides.
|
||||||
|
- `SkillStore` : CRUD round-trip en tmpdir pour les deux scopes ; un skill `Project` n'apparaît pas dans le scope `Global` et inversement.
|
||||||
|
- `AssignSkillToAgent` / `UnassignSkillFromAgent` : mutent l'`AgentManifest`, émettent `SkillAssigned`, idempotents (pas de doublon).
|
||||||
|
- **Injection** : le convention file généré contient bien le `content_md` des skills assignés et **rien** des skills non assignés ; ordre déterministe.
|
||||||
|
- Front : CRUD skills + assignation via gateway mock (RTL) ; garde-fou « no direct invoke ».
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (skill/store/app) + `vitest` verts ; cycle manuel : créer un skill, l'assigner à un agent, l'activer → le skill apparaît dans le convention file de `.ideai/run/<agent-id>/`.
|
||||||
|
- DoD commune (cf. README) respectée ; zéro régression.
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
### ✅ Domaine (vert)
|
||||||
|
- **Entité `Skill`** (`domain/skill.rs`) : `id: SkillId`, `name`, `content_md: MarkdownDoc`, `scope: SkillScope(Global|Project)`. Constructeur validant (`name` + `content_md` non vides), `with_content` re-valide l'invariant.
|
||||||
|
- **`SkillRef { skill_id, scope }`** : référence d'assignation portée par l'agent ; `From<&Skill>`.
|
||||||
|
- **`SkillId`** ajouté (`ids.rs`), event **`SkillAssigned { agent_id, skill_id, assigned }`** (`events.rs`), DTO + arm de mapping côté `app-tauri` (`events.rs`).
|
||||||
|
- **`Agent`** étendu : champ `skills: Vec<SkillRef>` (serde `default`), méthodes `assign_skill` (idempotent), `unassign_skill`, `with_skills` (dédup). **`ManifestEntry`** : champ `skills` (serde `default` + `skip_serializing_if` → rétrocompat des manifests pré-L12) ; `from_agent`/`to_agent` préservent les skills.
|
||||||
|
- **Tests** : 8 invariants (`entities.rs`) + 3 serde dont rétrocompat d'un manifest legacy sans clé `skills` (`serde_roundtrip.rs`). `cargo test -p domain` vert ; `cargo test --workspace` vert (0 régression) ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ Port + adapter store (vert)
|
||||||
|
- **Port `SkillStore`** (`domain/ports.rs`) : `list/get/save/delete` portant `scope` + `root: &ProjectPath` **par appel** (root ignoré pour `Global`, résolu pour `Project`) — un seul store sert tous les projets ouverts, comme `AgentContextStore`.
|
||||||
|
- **Adapter `FsSkillStore`** (`infrastructure/store/skill.rs`) : même forme on-disk que `FsTemplateStore` (`index.json` + `md/<id>.md`), deux racines disjointes : `<app_data>/skills/` (Global) et `<root>/.ideai/skills/` (Project). Delete laisse l'orphelin md (pas de remove dans le port FS), index = source de vérité. **7 tests** d'intégration tmpdir (`skill_store.rs`) : round-trip 2 scopes, **isolation de scope**, upsert, delete idempotent, camelCase.
|
||||||
|
|
||||||
|
### ✅ Use cases application (vert)
|
||||||
|
- `application/skill` : `CreateSkill`, `UpdateSkill`, `DeleteSkill`, `ListSkills(scope)` (inputs portant `project_root`), `AssignSkillToAgent` / `UnassignSkillFromAgent` (mutent l'`AgentManifest` via `to_agent`/`from_agent`, dédup, émettent `SkillAssigned`, **idempotents**). **9 tests** (`skill_usecases.rs`).
|
||||||
|
|
||||||
|
### ✅ Injection dans le convention file (vert, fil L6)
|
||||||
|
- `LaunchAgent` reçoit le port `SkillStore` ; `resolve_skills` lit les `.md` des skills assignés (ordre manifest, déterministe ; skill supprimé = `SkillRef` pendant → ignoré sans bloquer le lancement).
|
||||||
|
- `compose_convention_file` étendu : section `# Skills` (sous-titres `## <name>`) après le persona ; omise si aucun skill. **3 tests** unitaires + e2e (`agent_lifecycle.rs` : injection ordonnée, ref pendant tolérée).
|
||||||
|
- **Composition root** (`app-tauri/state.rs`) : `FsSkillStore` construit (app-data global), injecté dans `LaunchAgent`.
|
||||||
|
|
||||||
|
### ✅ IPC `app-tauri` (vert)
|
||||||
|
- **DTOs** (`dto.rs`) : `SkillDto` (transparent sur `Skill`, camelCase), `SkillListDto`, request DTOs (`Create/Update/Assign/UnassignSkillRequestDto`), `parse_skill_id`. `scope` désérialise directement vers `SkillScope` (`"global"`/`"project"`).
|
||||||
|
- **Commandes** (`commands.rs`) : `create_skill`, `update_skill`, `list_skills`, `delete_skill`, `assign_skill_to_agent`, `unassign_skill_from_agent` — shells fins qui résolvent le `Project` (→ `project.root`) puis appellent le use case. Enregistrées dans `lib.rs`.
|
||||||
|
- **Composition root** (`state.rs`) : 6 use cases skill câblés sur le `skill_store_port` (déjà construit pour le launcher) et le `contexts_port` partagé.
|
||||||
|
- `cargo build -p app-tauri` + `cargo test --workspace` (304) verts ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ Front `features/skills` (vert)
|
||||||
|
- **Domaine** (`domain/index.ts`) : `SkillScope`, `Skill`, `SkillRef` ; `Agent` étendu avec `skills: SkillRef[]`.
|
||||||
|
- **Port** (`ports/index.ts`) : `SkillGateway` (list/create/update/delete + assign/unassign) + `CreateSkillInput` ; ajouté à `Gateways`.
|
||||||
|
- **Adapters** : `TauriSkillGateway` (`adapters/skill.ts`, invoke camelCase) ; `MockSkillGateway` (`adapters/mock`, scopes disjoints + mutation partagée du `MockAgentGateway` via `_setSkills`, assign idempotent).
|
||||||
|
- **Feature** : `useSkills` (VM 2 scopes), `SkillEditor` (overlay md edit/preview + sélecteur de scope), `SkillsPanel` (listes Project/Global, CRUD). Onglet **Skills** ajouté dans `ProjectsView`.
|
||||||
|
- **Assignation** dans `AgentsPanel` : chips des skills assignés + sélecteur d'assignation + unassign, sur l'agent sélectionné ; refresh après mutation.
|
||||||
|
- **Tests** (`skills.test.tsx`, RTL via `DIProvider` + mocks) : CRUD project/global, isolation de scope, édition, suppression, assign/unassign reflétés sur l'agent, idempotence, **garde-fou « no direct invoke »** (aucune action run/launch). `vitest` : **229** verts (0 régression ; test « ten gateways » mis à jour).
|
||||||
|
|
||||||
|
### ⏳ Reste à faire
|
||||||
|
- Cycle manuel : créer un skill, l'assigner à un agent, l'activer → vérifier qu'il apparaît dans le convention file de `.ideai/run/<agent-id>/` (à faire sur l'AppImage).
|
||||||
35
agents-dev/L13-orchestrator.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# L13 — OrchestratorApi
|
||||||
|
|
||||||
|
**Binôme :** `dev-orchestrator` / `test-orchestrator`
|
||||||
|
**Zones :** `infrastructure/orchestrator`, `application/agent`, `app-tauri`
|
||||||
|
**Dépendances amont :** L0, L1, L6 (`LaunchAgent`/`StopAgent`), L12 (`update_agent_context` peut toucher les skills).
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Permettre à un **agent orchestrateur** de demander à IdeA de créer/arrêter/mettre à jour un agent — **exactement** comme l'utilisateur via l'UI. L'orchestrateur ne spawne jamais lui-même un process CLI : il **délègue à IdeA**, unique source de vérité du cycle de vie des agents. Cf. ARCHITECTURE §14.3.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- **Port `OrchestratorApi`** (adapter *entrant*, driven by file-watcher) : surveille `.ideai/requests/<requester-id>/`, désérialise les requêtes JSON, les traduit en appels de use cases.
|
||||||
|
- **Adapter `FsOrchestratorAdapter`** (`infrastructure/orchestrator`) : file-watching (`notify`), parse, dispatch, **supprime le fichier de requête** et **écrit une réponse** (succès/erreur) à côté.
|
||||||
|
- **Actions v1** : `spawn_agent` (→ `LaunchAgent`), `stop_agent` (→ `StopAgent`), `update_agent_context` (réécrit le `.md` de l'agent ± skills).
|
||||||
|
- **Event** : `OrchestratorRequest { requester_id, action }`.
|
||||||
|
- **Schéma requête** :
|
||||||
|
```json
|
||||||
|
{ "action": "spawn_agent", "name": "dev-backend", "profile": "claude-code", "context": "agents/dev-backend.md" }
|
||||||
|
```
|
||||||
|
- Le résultat d'un `spawn_agent` est **identique** à un lancement UI : cellule terminal créée, agent inscrit dans l'onglet Agents.
|
||||||
|
- **Composition root** (`app-tauri`) : démarrer le watcher, brancher sur les use cases existants ; arrêt propre à la fermeture.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Désérialisation : requêtes valides → action typée ; requête malformée → réponse d'erreur, **pas de crash**, pas de spawn.
|
||||||
|
- `spawn_agent` invoque `LaunchAgent` avec les bons args (use case mocké) ; idempotence sur double-dépôt du même fichier (traité une fois).
|
||||||
|
- Après traitement : fichier de requête **supprimé**, fichier de réponse écrit avec le bon statut.
|
||||||
|
- `stop_agent` / `update_agent_context` : mappent vers les bons use cases ; cible inexistante → erreur propre.
|
||||||
|
- Watcher : un fichier déposé dans `.ideai/requests/<id>/` est détecté (test d'intégration tmpdir + `notify`).
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (orchestrator/app) verts ; cycle manuel : un agent écrit un fichier de requête `spawn_agent` → un nouvel agent apparaît dans la grille et l'onglet Agents, fichier consommé + réponse écrite.
|
||||||
|
- Garde-fou : l'orchestrateur ne lance **aucun** process directement (vérifié par revue + absence de `ProcessSpawner` dans le chemin orchestrateur).
|
||||||
|
- DoD commune respectée ; zéro régression ; git reste optionnel (rien dans ce lot n'en dépend, §14.4).
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
⬜ À démarrer. Cadrage figé dans ARCHITECTURE §14.3.
|
||||||
22
agents-dev/L2-projects.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# L2 — Projets & stockage
|
||||||
|
|
||||||
|
**Binôme :** `dev-projects` / `test-projects`
|
||||||
|
**Zones :** `application/project`, `infrastructure/{fs,store}`, `frontend/features/projects`
|
||||||
|
**Dépendances amont :** L0, L1.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Gérer le cycle de vie des projets (création par project root, ouverture, fermeture) et le stockage de base.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Use cases : `CreateProject` (init `.ideai/` + `project.json` + registre), `OpenProject`, `CloseProject`/`CloseTab`.
|
||||||
|
- Adapters : `LocalFileSystem` (tokio::fs), `FsProjectStore` (registre projets + workspace en JSON dans données app).
|
||||||
|
- UI : sélection du project root, liste des projets, ouverture en onglet.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Use cases avec `FileSystem`/`ProjectStore` mockés : création initialise bien `.ideai/`, invariants projet respectés (root absolu, unicité `(remote, root)`).
|
||||||
|
- Intégration ciblée : `LocalFileSystem` sur tmpdir, `FsProjectStore` round-trip.
|
||||||
|
- Front : feature projects avec gateway mock (RTL).
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test -p application -p infrastructure` (filtré projet) + `vitest` verts.
|
||||||
|
- Créer/ouvrir/fermer un projet de bout en bout (avec adapters réels en dev manuel).
|
||||||
25
agents-dev/L3-terminals.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# L3 — Terminaux & PTY (local)
|
||||||
|
|
||||||
|
**Binôme :** `dev-terminals` / `test-terminals`
|
||||||
|
**Zones :** `infrastructure/pty`, `application/terminal`, `frontend/features/terminals`
|
||||||
|
**Dépendances amont :** L0, L1.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Terminaux fonctionnels en local : ouverture PTY, I/O, resize, fermeture, rendu xterm.js, flux via Tauri Channel.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Adapter `PortablePtyAdapter` (portable-pty) implémentant `PtyPort`.
|
||||||
|
- Use cases : `OpenTerminal`, `WriteToTerminal`, `ResizeTerminal`, `CloseTerminal`.
|
||||||
|
- Front : wrapper xterm.js, abonnement au flux d'octets (Channel), envoi des frappes/resize.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Use cases avec `PtyPort` mocké (spawn/write/resize/kill appelés correctement).
|
||||||
|
- Intégration : `PortablePtyAdapter` lance `echo`/`printf` et reçoit la sortie attendue.
|
||||||
|
- Front : wrapper xterm avec gateway mock (frappe → write, octets reçus → rendu).
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (pty/terminal) + `vitest` verts ; un terminal réel utilisable en dev manuel sur Linux.
|
||||||
|
|
||||||
|
## Spikes (cf. ARCHITECTURE §13)
|
||||||
|
- ConPTY Windows (resize/signaux/exit codes).
|
||||||
|
- Backpressure/coalescing du flux haute fréquence via Channel.
|
||||||
21
agents-dev/L4-layout.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# L4 — Layout tableur
|
||||||
|
|
||||||
|
**Binôme :** `dev-layout` / `test-layout`
|
||||||
|
**Zones :** `domain/layout` (déjà amorcé en L0), `application/layout`, `frontend/features/layout`
|
||||||
|
**Dépendances amont :** L0, L1, L3 (cellules ↔ terminaux).
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Grille redimensionnable type tableur : N colonnes par ligne / M lignes par colonne indépendantes, **fusion de cellules**, persistance.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Compléter la logique de layout pure du domaine (si reliquats post-L0).
|
||||||
|
- Use case `MutateLayout` (split/merge/resize/move) + persistance `.ideai/layout.json`.
|
||||||
|
- UI : grille redimensionnable (drag des séparateurs), création/suppression de cellules, fusion, mapping cellule → terminal.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Domaine : opérations pures exhaustives (déjà couvertes L0, étendre cas combinés).
|
||||||
|
- Application : `MutateLayout` persiste et publie `LayoutChanged`.
|
||||||
|
- Front : logique de calcul des tailles de cellules (Vitest, pure) ; interactions de split/merge (RTL + mock).
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (layout) + `vitest` verts ; manipulation visuelle de la grille fonctionnelle.
|
||||||
22
agents-dev/L5-ai-runtime.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# L5 — Profils IA & runtime
|
||||||
|
|
||||||
|
**Binôme :** `dev-ai-runtime` / `test-ai-runtime`
|
||||||
|
**Zones :** `infrastructure/runtime`, `application/agent`, `frontend/features/first-run`
|
||||||
|
**Dépendances amont :** L0, L1, L2.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Moteur IA flexible : profils déclaratifs, détection, first-run wizard. Cœur du « 100% IA, piloté par données ».
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Adapter `CliAgentRuntime` (un seul, piloté par `AgentProfile`) : `detect`, `prepare_invocation` (construit `SpawnSpec` + plan d'injection selon `ContextInjection`).
|
||||||
|
- Use cases : `DetectProfiles`, `ConfigureProfiles`.
|
||||||
|
- Stockage `profiles.json` (store global IDE).
|
||||||
|
- Front : **first-run wizard** demandant quels profils configurer ; commandes pré-remplies (Claude/Codex/Gemini/Aider) **éditables** ; ajout de **profil custom**.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- `CliAgentRuntime` : pour chaque stratégie d'injection (conventionFile/flag/stdin/env), le `SpawnSpec` produit est correct.
|
||||||
|
- `DetectProfiles` avec `ProcessSpawner` mocké (présent/absent).
|
||||||
|
- Front : wizard avec gateway mock (sélection, édition, ajout custom).
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (runtime/agent) + `vitest` verts ; wizard fonctionnel en dev manuel.
|
||||||
52
agents-dev/L6-agents.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# L6 — Agents & contextes
|
||||||
|
|
||||||
|
**Binôme :** `dev-agents` / `test-agents`
|
||||||
|
**Zones :** `application/agent`, `infrastructure/store`, `frontend/features/agents`
|
||||||
|
**Dépendances amont :** L0, L1, L2, L3, L5.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Agents de projet : contextes `.md` dans `.ideai/`, manifeste, et **lancement d'un agent** (injection contexte + spawn CLI dans une cellule terminal).
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Adapter `IdeaiContextStore` (compose `FileSystem`) : lecture/écriture `.md` + `agents.json`.
|
||||||
|
- Use cases : `CreateAgentFromScratch`, `LaunchAgent` (résout profil+contexte, injecte, ouvre cellule PTY au bon `cwd`, spawn CLI), CRUD agents.
|
||||||
|
- Front : panneau agents (créer, éditer le `.md`, activer → terminal).
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- `IdeaiContextStore` : round-trip `.md` + manifeste (intégration tmpdir + mock FS pour les use cases).
|
||||||
|
- `LaunchAgent` : ordre des appels (prepare_invocation → injection → pty.spawn) avec `cwd` correct.
|
||||||
|
- Front : feature agents avec gateway mock.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (agent/store) + `vitest` verts ; activer un agent ouvre un terminal avec la CLI lancée (dev manuel).
|
||||||
|
|
||||||
|
## Spike
|
||||||
|
- Injection `conventionFile` : symlink vs copie ; conflits si `CLAUDE.md` existe ; symlinks Windows.
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
### ✅ Backend (vert)
|
||||||
|
- **Domaine** : `ManifestEntry` réconcilié avec le schéma documenté `agents.json` (ARCHITECTURE §9.1) — porte désormais `name` + `profile_id` ; helpers `from_agent`/`to_agent` (le manifeste est la forme persistée d'un `Agent`). Tests domaine maj, verts.
|
||||||
|
- **Infra** : `IdeaiContextStore` (`infrastructure/store/context.rs`) implémente `AgentContextStore` en composant `FileSystem` ; écrit `.ideai/agents.json` + `.ideai/agents/*.md`, location-neutre (réutilisable local/SSH/WSL). Test d'intégration tmpdir (5 tests).
|
||||||
|
- **Application** (`application/agent/lifecycle.rs`) : `CreateAgentFromScratch`, `ListAgents`, `ReadAgentContext`, `UpdateAgentContext`, `DeleteAgent`, et `LaunchAgent` (résout profil+contexte → `prepare_invocation` → injection → `pty.spawn` au bon `cwd` → event `AgentLaunched`). 9 tests use cases (ordre d'appel vérifié via trace partagée).
|
||||||
|
- **Spike `conventionFile`** tranché pour L6 : **copie** du `.md` vers le fichier conventionnel (ex. `CLAUDE.md`), écrasement si présent — choix portable (symlinks Windows = privilèges, sémantique SFTP/WSL divergente). Stratégie `Env` → chemin absolu du `.md` ; `Stdin` → contenu piped après spawn.
|
||||||
|
|
||||||
|
### ✅ Front (vert)
|
||||||
|
- **Port** `AgentGateway` étendu (list/create/read/update/delete/launch) + type `Agent` dans `domain` ; **mock** stateful `MockAgentGateway` ; **adapter** Tauri `TauriAgentGateway` (`src/adapters/agent.ts`, commandes `*_agent` — câblage backend à venir).
|
||||||
|
- **Feature** `frontend/features/agents` : hook `useAgents(projectId)` + `AgentsPanel` (liste, création nom+profil, éditeur de contexte `.md`, Launch/Delete), bâti sur le **design system** (LD) et intégré dans l'onglet projet actif.
|
||||||
|
- **Tests** : 13 nouveaux (RTL + mock) ; suite front **116 verts**, `tsc` clean. Garde-fou « no direct invoke » respecté.
|
||||||
|
|
||||||
|
### ✅ IPC `app-tauri` (vert)
|
||||||
|
- **Composition root** (`state.rs`) : `IdeaiContextStore` construit ; 6 use cases agents instanciés en réutilisant les ports existants ; `LaunchAgent` partage le **même** `pty_port` + `terminal_sessions` que les terminaux (indispensable au `PtyBridge`) ; handle `project_store` ajouté pour résoudre le `Project` depuis un `projectId`.
|
||||||
|
- **Commands** (`commands.rs`) : `create_agent`, `list_agents`, `read_agent_context`, `update_agent_context`, `delete_agent`, `launch_agent`. `launch_agent` imite `open_terminal` (Channel + `PtyBridge` + thread de pompe). Enregistrées dans `lib.rs`.
|
||||||
|
- **DTOs** (`dto.rs`) : `AgentDto`/`AgentListDto` (transparent, camelCase), request DTOs, `parse_agent_id`, `From<LaunchAgentOutput> for TerminalSessionDto`.
|
||||||
|
- **Tests** : `tests/dto_agents.rs` (10) ; `cargo test -p app-tauri` 44 verts ; `cargo test --workspace` **256 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ Terminal d'agent (front, vert) — L6 clos
|
||||||
|
- `AgentGateway.launchAgent(projectId, agentId, options, onData)` renvoie désormais un `TerminalHandle` (signature calquée sur `openTerminal`) ; adapter Tauri = `Channel<number[]>` + `invoke("launch_agent", …)` + write/resize/close via les commandes terminal (clé `sessionId`) ; mock = greeting + echo.
|
||||||
|
- `TerminalView` généralisé avec une prop `open?` optionnelle (par défaut = gateway terminal) → réutilisé tel quel pour le terminal d'agent (xterm/fit/resize/cleanup partagés).
|
||||||
|
- `AgentsPanel` : bouton **Launch** monte un `TerminalView` (conteneur sombre `h-96`) branché sur la session d'agent ; bouton **Stop** le démonte (cleanup `close()`).
|
||||||
|
- **Tests** front : 120 verts (`tsc` clean), garde-fou « no direct invoke » respecté. Backend/IPC : 256 verts.
|
||||||
|
|
||||||
|
### ⏳ Hors périmètre L6 (à reprendre plus tard)
|
||||||
|
- Affiner la stratégie `Env` (support adapter de premier ordre).
|
||||||
42
agents-dev/L7-templates.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# L7 — Templates & synchronisation
|
||||||
|
|
||||||
|
**Binôme :** `dev-templates` / `test-templates`
|
||||||
|
**Zones :** `application/template`, `infrastructure/store`, `frontend/features/templates`
|
||||||
|
**Dépendances amont :** L0, L1, L5, L6.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Templates d'agents (store global IDE), versioning, création d'agent depuis template, **détection de drift** et **synchronisation template → agents**.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Adapter `FsTemplateStore` (md + `index.json`) avec versioning monotone + `content_hash`.
|
||||||
|
- Use cases : `CreateTemplate`, `UpdateTemplate` (bump version), `CreateAgentFromTemplate`, `DetectAgentDrift`, `SyncAgentWithTemplate` (remplacement du `.md` si `synchronized`).
|
||||||
|
- Front : gestion des templates, badge « MAJ disponible », action de sync.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- `UpdateTemplate` incrémente la version ; `DetectAgentDrift` détecte `version > synced_template_version`.
|
||||||
|
- `SyncAgentWithTemplate` : applique aux `synchronized==true`, ignore `false` et `scratch` ; met à jour `synced_template_version`.
|
||||||
|
- Front : badge drift + flux de sync avec gateway mock.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (template/store) + `vitest` verts ; cycle MAJ template → propagation aux agents synchronisés (dev manuel).
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
### ✅ Backend (vert)
|
||||||
|
- **Infra** : `FsTemplateStore` (`infrastructure/store/template.rs`) — store global `<app>/templates/{index.json, md/<id>.md}`, versioning persisté, `contentHash` (digest stable, sans dépendance) pour détection d'édition hors-app. 6 tests d'intégration (tmpdir).
|
||||||
|
- **Application** (`application/template/usecases.rs`) : `CreateTemplate`, `UpdateTemplate` (bump version + event `TemplateUpdated`), `ListTemplates`, `DeleteTemplate`, `CreateAgentFromTemplate` (origine `FromTemplate` + seed du `.md`, réutilise le helper de nommage L6), `DetectAgentDrift` (ne flague que les `synchronized` en retard ; ignore non-sync/scratch/à-jour/template supprimé ; émet `AgentDriftDetected`), `SyncAgentWithTemplate` (remplace le `.md`, avance `synced_template_version`, émet `AgentSynced` ; laisse intacts non-sync/scratch). 8 tests use cases.
|
||||||
|
- `cargo test --workspace` : **270 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ IPC `app-tauri` (vert)
|
||||||
|
- Composition root : `FsTemplateStore` construit, 7 use cases injectés (réutilise `contexts_port`/`ids`/`events_port`).
|
||||||
|
- 7 commands : `create_template`, `update_template`, `list_templates`, `delete_template`, `create_agent_from_template`, `detect_agent_drift`, `sync_agent_with_template` (shells fins, `resolve_project` réutilisé). DTOs camelCase (`TemplateDto` transparent, `AgentDriftDto`, `SyncResultDto`, `parse_template_id`).
|
||||||
|
- Tests `tests/dto_templates.rs` (18) ; `cargo test -p app-tauri` 62 verts ; workspace **288 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ Front (vert)
|
||||||
|
- Port `TemplateGateway` (7 méthodes) + types `Template`/`AgentDrift` ; adapter Tauri `TauriTemplateGateway` ; `MockTemplateGateway` stateful **partageant le registre d'agents** du `MockAgentGateway` (helpers internes `_insertAgent`/`_updateAgent`/`_rawAgents`) pour faire vivre le drift offline.
|
||||||
|
- Feature `features/templates` : `useTemplates` (CRUD), `useDrift(projectId)` (détection + sync), `TemplatesPanel` (liste nom+version, création, édition, suppression, « Create agent from template »), monté dans l'onglet projet.
|
||||||
|
- `AgentsPanel` : **badge « update available »** + bouton **Sync** par agent en drift.
|
||||||
|
- Tests : 20 ajoutés ; suite front **140 verts** ; `tsc` clean ; garde-fou « no direct invoke » respecté.
|
||||||
|
|
||||||
|
### ⏳ Hors périmètre L7
|
||||||
|
- Intégration front du terminal d'agent depuis un agent créé via template (réutilise le fil L6).
|
||||||
42
agents-dev/L8-git.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# L8 — Git
|
||||||
|
|
||||||
|
**Binôme :** `dev-git` / `test-git`
|
||||||
|
**Zones :** `infrastructure/git`, `application/git`, `frontend/features/git`
|
||||||
|
**Dépendances amont :** L0, L1, L2.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Support Git intégré (local) via libgit2.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Adapter `Git2Repository` (git2) : `status`, `stage`/`unstage`, `commit`, `branches`, `checkout`, `current_branch`, `diff`, `log`, `pull`, `push`, `clone`, `init`.
|
||||||
|
- Use cases Git (`GitStatus`, `GitCommit`, `GitCheckout`, `GitPush`, …).
|
||||||
|
- Front : panneau Git (changements, staging, commit, branches).
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Use cases avec `GitRepository` mocké.
|
||||||
|
- Intégration : `Git2Repository` sur repo temporaire (init → commit → branch → status).
|
||||||
|
- Front : feature git avec gateway mock.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (git) + `vitest` verts ; opérations git de base utilisables (dev manuel).
|
||||||
|
|
||||||
|
## Spike
|
||||||
|
- Vendoring statique git2/openssl pour l'AppImage (coordonné avec L11).
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
### ✅ Backend (vert)
|
||||||
|
- **Dépendance** : `git2` 0.20 (`default-features = false`, pas d'openssl) liée à la **libgit2 système** via `.cargo/config.toml` (`LIBGIT2_SYS_USE_PKG_CONFIG=1`) — évite cmake/vendoring ; le vendoring statique AppImage reste pour L11.
|
||||||
|
- **Infra** : `Git2Repository` (`infrastructure/git/mod.rs`) implémente `GitPort` — `init`, `status`, `stage`, `unstage`, `commit` (signature de repli `IdeA <idea@localhost>` si pas de config user), `branches`, `current_branch`, `checkout`, `log`. `pull`/`push` renvoient une erreur explicite (réseau/credentials → L9). Appels libgit2 synchrones sans `await` ⇒ futures `Send`. 3 tests d'intégration (vrai repo temporaire).
|
||||||
|
- **Application** (`application/git/`) : `GitStatus`, `GitStage`, `GitUnstage`, `GitCommit` (valide message non vide + event `GitStateChanged`), `GitBranches` (liste + courante), `GitCheckout` (+ event), `GitLog`, `GitInit` (+ event). 7 tests use cases (mock `GitPort`).
|
||||||
|
- `cargo test --workspace` : **298 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ IPC `app-tauri` (vert)
|
||||||
|
- Composition root : `Git2Repository` construit, 8 use cases injectés. 8 commands (`git_status`, `git_stage`, `git_unstage`, `git_commit`, `git_branches`, `git_checkout`, `git_log`, `git_init`), résolution `projectId → root` via `resolve_project`. DTOs `GitFileStatusDto`/`GitCommitDto`/`GitBranchesDto` + request DTOs camelCase. Tests `tests/dto_git.rs` (12). `cargo test -p app-tauri` 74 verts ; workspace **310 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ✅ Front (vert)
|
||||||
|
- Port `GitGateway` complet (8 méthodes) + types `GitFileStatus`/`GitCommit`/`GitBranches` ; adapter `TauriGitGateway` ; `MockGitGateway` stateful (état par projet, seedé) pour le mode offline.
|
||||||
|
- Feature `features/git` : `useGit(projectId)` + `GitPanel` (sections Staged/Unstaged + stage/unstage, message + commit, branches + checkout, log récent), monté dans l'onglet projet. 17 tests ; suite front **157 verts** ; `tsc` clean ; garde-fou « no invoke » OK.
|
||||||
|
|
||||||
|
### ⏳ Hors périmètre L8
|
||||||
|
- `pull`/`push`/`clone` (réseau + credentials) → L9 (`RemoteGitRepository` + callbacks) ; vendoring statique git2 pour l'AppImage → L11.
|
||||||
40
agents-dev/L9-remote.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# L9 — Remote (SSH + WSL)
|
||||||
|
|
||||||
|
**Binôme :** `dev-remote` / `test-remote`
|
||||||
|
**Zones :** `infrastructure/remote`, `application/remote`, `frontend/features/remote`
|
||||||
|
**Dépendances amont :** L0, L1, L2, L3, L8.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Développement distant : projet sur autre machine via **SSH**, ou sur une **WSL** depuis Windows. Transparence local/distant via la stratégie `RemoteHost`.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- `RemoteHost` : `LocalHost`, `SshHost` (russh/ssh2), `WslHost` (`wsl.exe`) — fabriquent FS/PTY/Spawner adaptés.
|
||||||
|
- Adapters distants : `SshFileSystem` (SFTP), `SshPtyAdapter`, `SshProcessSpawner` ; `WslFileSystem`, `WslPtyAdapter`, `WslProcessSpawner`.
|
||||||
|
- `RemoteGitRepository` (git CLI via `ProcessSpawner`) en fallback distant.
|
||||||
|
- Use case `ConnectRemote` (valide l'accès au root). Front : UI de connexion SSH/WSL.
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- Substituabilité (Liskov) : un use case marche identiquement quel que soit le `RemoteHost` (tests avec hosts mockés).
|
||||||
|
- Intégration SSH/WSL : tests `#[ignore]` gated derrière feature/env (CI conditionnelle).
|
||||||
|
- Front : feature remote avec gateway mock.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `cargo test` (remote, hors `#[ignore]`) + `vitest` verts ; connexion SSH et WSL démontrée en dev manuel.
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
### ✅ Socle (vert)
|
||||||
|
- **Domaine** : `RemoteRef` (Local/Ssh/Wsl) + port `RemoteHost` (fabrique FS/PTY/Spawner) déjà en place (L0).
|
||||||
|
- **Infra** (`infrastructure/remote/mod.rs`) : `LocalHost` (fabrique les adapters locaux ; `connect` = no-op) + sélecteur `remote_host(&RemoteRef)` (Local → `LocalHost` ; SSH/WSL → `RemoteError::Connection` explicite « not yet supported »). 4 tests d'intégration.
|
||||||
|
- **Application** (`application/remote/`) : `ConnectRemote` (établit l'hôte, valide que le root existe via le FS de l'hôte, émet `RemoteConnected`). 3 tests **Liskov** (comportement identique Local/Ssh/Wsl avec hôte mocké ; échec connexion → REMOTE ; root absent → NOT_FOUND).
|
||||||
|
- `cargo test --workspace` : **317 verts, 0 régression** ; clippy clean.
|
||||||
|
|
||||||
|
### ⏳ Reste à faire (gated / non vérifiable dans cet environnement)
|
||||||
|
- Adapters distants **SSH** (`SshHost`/`SshFileSystem` SFTP/`SshPtyAdapter`/`SshProcessSpawner` via russh ou ssh2 — décision auth à figer, cf. spikes) et **WSL** (`WslHost` + adapters préfixant `wsl.exe`), avec tests d'intégration `#[ignore]` derrière feature/env.
|
||||||
|
- `RemoteGitRepository` (git CLI distant via `ProcessSpawner`) + `pull`/`push` (reportés depuis L8).
|
||||||
|
- IPC `app-tauri` `connect_remote` + front feature remote (UI connexion SSH/WSL).
|
||||||
|
|
||||||
|
## Spikes (cf. ARCHITECTURE §13)
|
||||||
|
- Auth SSH (clé/agent/mot de passe/known_hosts) ; choix russh(rustls) vs ssh2(OpenSSL) — impacte l'AppImage.
|
||||||
|
- Conversion de chemins WSL `/mnt/...` ↔ `\\wsl$\...` ; perf I/O cross-boundary.
|
||||||
|
- Git sur FS distant (perf, parsing CLI).
|
||||||
39
agents-dev/LD-design-system.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# LD — Design System (UI kit maison)
|
||||||
|
|
||||||
|
**Binôme :** `dev-ui` / `test-ui`
|
||||||
|
**Zones :** `frontend/src/shared` (UI kit), `frontend` (config Tailwind), `frontend/src/app` (app shell)
|
||||||
|
**Dépendances amont :** L1 (frontend ports/adapters/DI en place).
|
||||||
|
**Position :** inséré **après L6** (décision produit : voir CONTEXT, lot transverse), consommé ensuite au fil de l'eau par chaque feature.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Doter IdeA d'un **design system maison** cohérent et d'un **thème sombre** par défaut (c'est un IDE), pour que chaque feature cesse de bricoler des styles inline et consomme des composants réutilisables.
|
||||||
|
|
||||||
|
## Stack (validée)
|
||||||
|
- **Tailwind v4** + `@tailwindcss/vite` (CSS-first, `@theme`), composants **maison** dans `shared/ui` (pas de lib de composants tierce).
|
||||||
|
- Tokens de design (couleurs/typo/espacements/rayons) en variables de thème ; helper `cn()` pour composer les classes.
|
||||||
|
|
||||||
|
## Périmètre (DEV)
|
||||||
|
- Config Tailwind (plugin Vite, feuille de thème globale, tokens, dark par défaut).
|
||||||
|
- `shared/ui` : composants de base — `Button`, `IconButton`, `Input`, `Panel`/`Card`, `Tabs`, `Toolbar`, `Spinner`, `Field`. Helper `cn`.
|
||||||
|
- **App shell** sombre dans `app/` (remplace les styles inline du smoke-test).
|
||||||
|
|
||||||
|
## Périmètre (TEST)
|
||||||
|
- `cn()` (unitaire).
|
||||||
|
- Rendu + variantes/états des composants (`Button` variants/disabled, `Input` label/aria, `Tabs` sélection) via RTL/vitest, **sans backend**.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `vitest` vert ; `tsc --noEmit` vert ; le domaine/les ports UI restent inchangés (pur skin).
|
||||||
|
- Aucune feature ne régresse ; l'app monte avec le thème sombre.
|
||||||
|
|
||||||
|
## Hors périmètre
|
||||||
|
- Re-styling exhaustif de toutes les features (fait au fil de l'eau quand on touche chacune).
|
||||||
|
|
||||||
|
## Avancement — ✅ vert
|
||||||
|
|
||||||
|
- **Config** : `tailwindcss@4` + `@tailwindcss/vite` ajoutés ; plugin câblé dans `vite.config.ts` ; feuille de thème globale `src/shared/styles/theme.css` (tokens sémantiques sous `@theme`, thème **sombre** par défaut, focus ring), importée une fois dans `app/main.tsx`.
|
||||||
|
- **UI kit** `src/shared/ui` : `Button` (variants primary/secondary/ghost/danger, sizes, loading), `IconButton`, `Input`, `Field` (label/hint/error + aria), `Panel`, `Tabs` (sélection + close), `Toolbar`, `Spinner`. Helper `cn()`. Baril `@/shared`.
|
||||||
|
- **App shell** sombre (`app/App.tsx`) : header avec statut backend, plus de styles inline.
|
||||||
|
- **Consommation** : `features/projects/ProjectsView` migré sur le kit (premier consommateur), hooks d'accessibilité préservés (tests L2 toujours verts).
|
||||||
|
- **Tests** : `cn` + composants (`Button`/`Input`/`Field`/`Tabs`) via RTL — assertions sur rôles/aria, pas sur les classes Tailwind (le design peut évoluer sans casser la suite).
|
||||||
|
|
||||||
|
**Vérifs** : `tsc --noEmit` vert · `vitest` **103 tests verts** (14 fichiers) · `vite build` OK (Tailwind compile, CSS 22.8 kB).
|
||||||
71
agents-dev/README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Agents de développement IdeA — Protocole commun
|
||||||
|
|
||||||
|
> Ces agents servent à **développer l'IDE IdeA lui-même**. Ils ne sont pas la feature produit « agents IA » de l'application.
|
||||||
|
> Référence produit/archi : [`../CONTEXT.md`](../CONTEXT.md) · [`../ARCHITECTURE.md`](../ARCHITECTURE.md).
|
||||||
|
|
||||||
|
## Composition de l'équipe
|
||||||
|
|
||||||
|
Chaque **lot livrable** (L0…L11) est confié à un **binôme** :
|
||||||
|
- un **agent de développement** (écrit le code),
|
||||||
|
- un **agent de test** appairé (écrit et exécute les tests unitaires).
|
||||||
|
|
||||||
|
Un fichier `Lx-*.md` par binôme décrit son périmètre, ses ports/adapters, ses dépendances et sa *definition of done*.
|
||||||
|
|
||||||
|
| Lot | Fichier | Statut |
|
||||||
|
|---|---|---|
|
||||||
|
| L0 | [L0-core-domain.md](L0-core-domain.md) | ✅ **vert** (84 tests) |
|
||||||
|
| L1 | [L1-ipc-bridge.md](L1-ipc-bridge.md) | ✅ **vert** (46 tests) |
|
||||||
|
| L2 | [L2-projects.md](L2-projects.md) | ✅ **vert** (26 tests) |
|
||||||
|
| L3 | [L3-terminals.md](L3-terminals.md) | ✅ **vert** (61 tests) |
|
||||||
|
| L4 | [L4-layout.md](L4-layout.md) | ✅ **vert** (52 tests) |
|
||||||
|
| L5 | [L5-ai-runtime.md](L5-ai-runtime.md) | ✅ **vert** (69 tests) |
|
||||||
|
| L6 | [L6-agents.md](L6-agents.md) | ✅ **vert** (domaine+app+infra+IPC+front · activer un agent ouvre son terminal) |
|
||||||
|
| LD | [LD-design-system.md](LD-design-system.md) | ✅ **vert** (Tailwind v4 + UI kit maison, thème sombre) |
|
||||||
|
| L7 | [L7-templates.md](L7-templates.md) | ✅ **vert** (backend + IPC + front · drift & sync) |
|
||||||
|
| L8 | [L8-git.md](L8-git.md) | ✅ **vert** (backend + IPC + front · git local libgit2) |
|
||||||
|
| L9 | [L9-remote.md](L9-remote.md) | 🟡 **socle vert** (LocalHost + ConnectRemote, Liskov) · SSH/WSL gated à venir |
|
||||||
|
| L10 | [L10-windows.md](L10-windows.md) | 🟡 **backend + IPC verts** (move-tab + `WebviewWindow`) · détach UI fait avec la refonte L11 |
|
||||||
|
| L11 | [L11-packaging.md](L11-packaging.md) | 🟡 AppImage Linux **OK** · refonte disposition IDE en cours · Windows/CI à venir |
|
||||||
|
| L12 | [L12-skills.md](L12-skills.md) | ⬜ **Skills** — entité + store + assignation + injection convention file + UI |
|
||||||
|
| L13 | [L13-orchestrator.md](L13-orchestrator.md) | ⬜ **OrchestratorApi** — file-watcher, spawn depuis agent ou UI, même résultat |
|
||||||
|
|
||||||
|
## Cycle dev ↔ test (obligatoire, cf. CONTEXT §3)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Archi valide le découpage et les contrats (ports/interfaces) du lot.
|
||||||
|
2. Agent DEV écrit le code de la feature.
|
||||||
|
3. Agent TEST écrit les tests unitaires + les exécute.
|
||||||
|
4a. Vert → feature validée, lot suivant.
|
||||||
|
4b. Rouge → rapport d'erreurs structuré → retour DEV → correction → retour étape 3.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règle d'or** : aucune feature n'est « terminée » tant que ses tests ne passent pas. Les résultats sont relayés fidèlement (sortie réelle des tests).
|
||||||
|
|
||||||
|
## Definition of Done (commune à tous les lots)
|
||||||
|
|
||||||
|
- [ ] Code conforme à l'architecture hexagonale et SOLID (cf. ARCHITECTURE §1).
|
||||||
|
- [ ] Le `domain` reste pur (aucune dépendance I/O qui y entre).
|
||||||
|
- [ ] Les use cases ne parlent qu'aux **ports**, jamais aux adapters concrets.
|
||||||
|
- [ ] Tests unitaires écrits et **verts** : `cargo test -p <crate>` (Rust) et/ou `vitest` (front).
|
||||||
|
- [ ] Domaine/application testés **sans I/O** (ports mockés : `mockall` ou fakes).
|
||||||
|
- [ ] Pas de `new ConcreteAdapter` ailleurs que dans la composition root (`app-tauri`).
|
||||||
|
- [ ] Pas de régression sur les lots déjà verts.
|
||||||
|
- [ ] Code lisible, cohérent avec le style existant.
|
||||||
|
|
||||||
|
## Format du rapport d'erreurs (TEST → DEV)
|
||||||
|
|
||||||
|
```
|
||||||
|
LOT: Lx
|
||||||
|
TEST EN ÉCHEC: <nom du test>
|
||||||
|
ATTENDU: <…>
|
||||||
|
OBTENU: <…>
|
||||||
|
SORTIE: <extrait pertinent de cargo test / vitest>
|
||||||
|
HYPOTHÈSE: <cause probable, si identifiable>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions techniques
|
||||||
|
|
||||||
|
- Rust : workspace multi-crate (`crates/domain`, `application`, `infrastructure`, `app-tauri`).
|
||||||
|
- Erreurs typées par port/use case ; `async` via `async_trait` côté ports I/O.
|
||||||
|
- Déterminisme des tests via ports `Clock`/`IdGenerator` (impl `Fixed`/`Seq` en test).
|
||||||
|
- Front : `domain`/`ports` purs (Vitest), `features` testés avec gateways **mock** (RTL).
|
||||||
35
crates/app-tauri/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
name = "app-tauri"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
description = "IdeA — Tauri v2 shell: composition root (DI), IPC commands/events, PTY↔Channel bridge."
|
||||||
|
|
||||||
|
# The library carries all the wiring so it is unit-testable; the binary is a
|
||||||
|
# thin entry point.
|
||||||
|
[lib]
|
||||||
|
name = "app_tauri_lib"
|
||||||
|
crate-type = ["lib", "cdylib", "staticlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "app-tauri"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
domain = { workspace = true }
|
||||||
|
application = { workspace = true }
|
||||||
|
infrastructure = { workspace = true }
|
||||||
|
tauri = { workspace = true }
|
||||||
|
tauri-plugin-dialog = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
uuid = { workspace = true }
|
||||||
3
crates/app-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build();
|
||||||
|
}
|
||||||
7
crates/app-tauri/capabilities/default.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability set for the IdeA main window.",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default", "dialog:allow-open"]
|
||||||
|
}
|
||||||
1
crates/app-tauri/gen/schemas/acl-manifests.json
Normal file
1
crates/app-tauri/gen/schemas/capabilities.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Default capability set for the IdeA main window.","local":true,"windows":["main"],"permissions":["core:default","dialog:allow-open"]}}
|
||||||
2358
crates/app-tauri/gen/schemas/desktop-schema.json
Normal file
2358
crates/app-tauri/gen/schemas/linux-schema.json
Normal file
BIN
crates/app-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
crates/app-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
crates/app-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
crates/app-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
crates/app-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
crates/app-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
crates/app-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
crates/app-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
crates/app-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
crates/app-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
crates/app-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
crates/app-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
crates/app-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
crates/app-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
crates/app-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
BIN
crates/app-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
crates/app-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
BIN
crates/app-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
crates/app-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
BIN
crates/app-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
BIN
crates/app-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
BIN
crates/app-tauri/icons/icon.icns
Normal file
BIN
crates/app-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
crates/app-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
crates/app-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
crates/app-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1342
crates/app-tauri/src/commands.rs
Normal file
1448
crates/app-tauri/src/dto.rs
Normal file
205
crates/app-tauri/src/events.rs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
//! `TauriEventRelay` — bridges the domain [`EventBus`] to Tauri events
|
||||||
|
//! (backend → frontend push channel, ARCHITECTURE §2 "Events").
|
||||||
|
//!
|
||||||
|
//! The relay subscribes to the bus and re-emits each [`DomainEvent`] as a Tauri
|
||||||
|
//! event named [`DOMAIN_EVENT`], carrying a serialisable [`DomainEventDto`]
|
||||||
|
//! payload (the domain event itself is deliberately not `Serialize`; the wire
|
||||||
|
//! format is owned here, the infrastructure/presentation layer).
|
||||||
|
//!
|
||||||
|
//! High-frequency `PtyOutput` is intentionally *not* relayed through this global
|
||||||
|
//! event; it goes through per-session [`crate::pty::PtyBridge`] channels instead.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
use domain::events::DomainEvent;
|
||||||
|
use infrastructure::TokioBroadcastEventBus;
|
||||||
|
|
||||||
|
/// Name of the Tauri event carrying relayed [`DomainEvent`]s.
|
||||||
|
pub const DOMAIN_EVENT: &str = "domain://event";
|
||||||
|
|
||||||
|
/// Serialisable mirror of [`DomainEvent`] for the IPC wire (camelCase, tagged).
|
||||||
|
///
|
||||||
|
/// `type` is the discriminant; payload fields are flattened per variant. This is
|
||||||
|
/// the single owner of the event wire format on the backend side.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum DomainEventDto {
|
||||||
|
/// A project was created.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ProjectCreated {
|
||||||
|
/// Project id (UUID string).
|
||||||
|
project_id: String,
|
||||||
|
},
|
||||||
|
/// An agent was launched.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentLaunched {
|
||||||
|
/// Agent id.
|
||||||
|
agent_id: String,
|
||||||
|
/// Session id.
|
||||||
|
session_id: String,
|
||||||
|
},
|
||||||
|
/// An agent exited.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentExited {
|
||||||
|
/// Agent id.
|
||||||
|
agent_id: String,
|
||||||
|
/// Exit code.
|
||||||
|
code: i32,
|
||||||
|
},
|
||||||
|
/// A template was updated.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
TemplateUpdated {
|
||||||
|
/// Template id.
|
||||||
|
template_id: String,
|
||||||
|
/// New version.
|
||||||
|
version: u64,
|
||||||
|
},
|
||||||
|
/// A synchronized agent drifted from its template.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentDriftDetected {
|
||||||
|
/// Agent id.
|
||||||
|
agent_id: String,
|
||||||
|
/// Current version.
|
||||||
|
from: u64,
|
||||||
|
/// Available version.
|
||||||
|
to: u64,
|
||||||
|
},
|
||||||
|
/// A synchronized agent was brought up to date.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentSynced {
|
||||||
|
/// Agent id.
|
||||||
|
agent_id: String,
|
||||||
|
/// Version synced to.
|
||||||
|
to: u64,
|
||||||
|
},
|
||||||
|
/// A skill was assigned to (or unassigned from) an agent.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
SkillAssigned {
|
||||||
|
/// Agent id.
|
||||||
|
agent_id: String,
|
||||||
|
/// Skill id.
|
||||||
|
skill_id: String,
|
||||||
|
/// `true` if assigned, `false` if unassigned.
|
||||||
|
assigned: bool,
|
||||||
|
},
|
||||||
|
/// A tab's layout changed.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
LayoutChanged {
|
||||||
|
/// Project id.
|
||||||
|
project_id: String,
|
||||||
|
},
|
||||||
|
/// A remote connection was established.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
RemoteConnected {
|
||||||
|
/// Project id.
|
||||||
|
project_id: String,
|
||||||
|
},
|
||||||
|
/// Git state changed.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
GitStateChanged {
|
||||||
|
/// Project id.
|
||||||
|
project_id: String,
|
||||||
|
},
|
||||||
|
/// Raw PTY output (normally routed to a per-session channel, not here).
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
PtyOutput {
|
||||||
|
/// Session id.
|
||||||
|
session_id: String,
|
||||||
|
/// Output bytes.
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&DomainEvent> for DomainEventDto {
|
||||||
|
fn from(e: &DomainEvent) -> Self {
|
||||||
|
match e {
|
||||||
|
DomainEvent::ProjectCreated { project_id } => Self::ProjectCreated {
|
||||||
|
project_id: project_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::AgentLaunched {
|
||||||
|
agent_id,
|
||||||
|
session_id,
|
||||||
|
} => Self::AgentLaunched {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
session_id: session_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::AgentExited { agent_id, code } => Self::AgentExited {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
code: *code,
|
||||||
|
},
|
||||||
|
DomainEvent::TemplateUpdated {
|
||||||
|
template_id,
|
||||||
|
version,
|
||||||
|
} => Self::TemplateUpdated {
|
||||||
|
template_id: template_id.to_string(),
|
||||||
|
version: version.get(),
|
||||||
|
},
|
||||||
|
DomainEvent::AgentDriftDetected {
|
||||||
|
agent_id,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
} => Self::AgentDriftDetected {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
from: from.get(),
|
||||||
|
to: to.get(),
|
||||||
|
},
|
||||||
|
DomainEvent::AgentSynced { agent_id, to } => Self::AgentSynced {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
to: to.get(),
|
||||||
|
},
|
||||||
|
DomainEvent::SkillAssigned {
|
||||||
|
agent_id,
|
||||||
|
skill_id,
|
||||||
|
assigned,
|
||||||
|
} => Self::SkillAssigned {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
skill_id: skill_id.to_string(),
|
||||||
|
assigned: *assigned,
|
||||||
|
},
|
||||||
|
DomainEvent::LayoutChanged { project_id } => Self::LayoutChanged {
|
||||||
|
project_id: project_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::RemoteConnected { project_id } => Self::RemoteConnected {
|
||||||
|
project_id: project_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::GitStateChanged { project_id } => Self::GitStateChanged {
|
||||||
|
project_id: project_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::PtyOutput { session_id, bytes } => Self::PtyOutput {
|
||||||
|
session_id: session_id.to_string(),
|
||||||
|
bytes: bytes.clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes the relay to the bus and spawns a background task that forwards
|
||||||
|
/// every [`DomainEvent`] to the frontend as a [`DOMAIN_EVENT`] Tauri event.
|
||||||
|
///
|
||||||
|
/// Uses the bus's raw async broadcast receiver so the relay runs cooperatively
|
||||||
|
/// on the Tokio runtime (no blocking thread). Returns immediately; the spawned
|
||||||
|
/// task lives for the duration of the app.
|
||||||
|
pub fn spawn_relay(app: AppHandle, bus: &TokioBroadcastEventBus) {
|
||||||
|
use tokio::sync::broadcast::error::RecvError;
|
||||||
|
|
||||||
|
let mut rx = bus.raw_receiver();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(event) => {
|
||||||
|
// Skip high-frequency PTY output on the global channel.
|
||||||
|
if matches!(event, DomainEvent::PtyOutput { .. }) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dto = DomainEventDto::from(&event);
|
||||||
|
let _ = app.emit(DOMAIN_EVENT, dto);
|
||||||
|
}
|
||||||
|
// The bus dropped some events for this slow receiver; keep going.
|
||||||
|
Err(RecvError::Lagged(_)) => continue,
|
||||||
|
// The bus was dropped (app shutting down); stop the relay.
|
||||||
|
Err(RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
132
crates/app-tauri/src/lib.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
//! # IdeA — `app-tauri` (presentation / driving adapter + composition root)
|
||||||
|
//!
|
||||||
|
//! This crate is the **only** place that knows every other crate. It:
|
||||||
|
//! - builds the concrete adapters and injects them into use cases
|
||||||
|
//! ([`state::AppState`], the composition root),
|
||||||
|
//! - exposes `#[tauri::command]` handlers ([`commands`]) mapping DTOs ↔ use cases,
|
||||||
|
//! - relays domain events to the frontend ([`events::TauriEventRelay`]),
|
||||||
|
//! - hosts the generic PTY↔Channel bridge ([`pty::PtyBridge`]) for L3.
|
||||||
|
//!
|
||||||
|
//! The wiring lives in the library (testable) and `main.rs` is a thin shim.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
pub mod dto;
|
||||||
|
pub mod events;
|
||||||
|
pub mod pty;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
use state::AppState;
|
||||||
|
|
||||||
|
/// Builds and runs the Tauri application.
|
||||||
|
///
|
||||||
|
/// Sets up the composition root (resolving the app-data directory via the Tauri
|
||||||
|
/// path API), registers commands, spawns the event relay, and starts the main
|
||||||
|
/// window.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if the Tauri application fails to build or run (no window/webview), or
|
||||||
|
/// if the app-data directory cannot be resolved.
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.setup(|app| {
|
||||||
|
// Resolve the machine-local IDE data directory (ARCHITECTURE §9.2)
|
||||||
|
// and build the composition root once the app handle exists, so the
|
||||||
|
// stores receive a concrete path without ever touching Tauri.
|
||||||
|
let app_data_dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.expect("failed to resolve the app data directory");
|
||||||
|
let app_state = AppState::build(app_data_dir);
|
||||||
|
|
||||||
|
// Wire the domain event bus → Tauri events relay.
|
||||||
|
events::spawn_relay(app.handle().clone(), &app_state.event_bus);
|
||||||
|
|
||||||
|
app.manage(app_state);
|
||||||
|
|
||||||
|
// Kill all live PTYs cleanly when the main window is closing. This is
|
||||||
|
// independent of the per-view (navigation/layout) lifecycle — those
|
||||||
|
// must NEVER kill a PTY — and only fires on a genuine app shutdown.
|
||||||
|
// A brutal crash is best-effort and out of scope.
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||||
|
if let Some(state) = handle.try_state::<AppState>() {
|
||||||
|
let pty = std::sync::Arc::clone(&state.pty_port);
|
||||||
|
let handles = state.terminal_sessions.handles();
|
||||||
|
tauri::async_runtime::block_on(async move {
|
||||||
|
for h in handles {
|
||||||
|
let _ = pty.kill(&h).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::health,
|
||||||
|
commands::create_project,
|
||||||
|
commands::open_project,
|
||||||
|
commands::close_project,
|
||||||
|
commands::list_projects,
|
||||||
|
commands::open_terminal,
|
||||||
|
commands::write_terminal,
|
||||||
|
commands::resize_terminal,
|
||||||
|
commands::close_terminal,
|
||||||
|
commands::reattach_terminal,
|
||||||
|
commands::load_layout,
|
||||||
|
commands::mutate_layout,
|
||||||
|
commands::list_layouts,
|
||||||
|
commands::create_layout,
|
||||||
|
commands::rename_layout,
|
||||||
|
commands::delete_layout,
|
||||||
|
commands::set_active_layout,
|
||||||
|
commands::first_run_state,
|
||||||
|
commands::reference_profiles,
|
||||||
|
commands::detect_profiles,
|
||||||
|
commands::list_profiles,
|
||||||
|
commands::save_profile,
|
||||||
|
commands::delete_profile,
|
||||||
|
commands::configure_profiles,
|
||||||
|
commands::create_agent,
|
||||||
|
commands::list_agents,
|
||||||
|
commands::read_agent_context,
|
||||||
|
commands::update_agent_context,
|
||||||
|
commands::delete_agent,
|
||||||
|
commands::launch_agent,
|
||||||
|
commands::create_template,
|
||||||
|
commands::update_template,
|
||||||
|
commands::list_templates,
|
||||||
|
commands::delete_template,
|
||||||
|
commands::create_agent_from_template,
|
||||||
|
commands::detect_agent_drift,
|
||||||
|
commands::sync_agent_with_template,
|
||||||
|
commands::git_status,
|
||||||
|
commands::git_stage,
|
||||||
|
commands::git_unstage,
|
||||||
|
commands::git_commit,
|
||||||
|
commands::git_branches,
|
||||||
|
commands::git_checkout,
|
||||||
|
commands::git_log,
|
||||||
|
commands::git_init,
|
||||||
|
commands::git_graph,
|
||||||
|
commands::create_skill,
|
||||||
|
commands::update_skill,
|
||||||
|
commands::list_skills,
|
||||||
|
commands::delete_skill,
|
||||||
|
commands::assign_skill_to_agent,
|
||||||
|
commands::unassign_skill_from_agent,
|
||||||
|
commands::move_tab_to_new_window,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running IdeA Tauri application");
|
||||||
|
}
|
||||||
16
crates/app-tauri/src/main.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Prevents an extra console window on Windows in release builds.
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// WebKitGTK's DMABUF renderer causes a blank/white window on many Linux
|
||||||
|
// setups (recent Mesa/Nvidia drivers, common on Arch). Disable it before
|
||||||
|
// the webview initializes, unless the user has explicitly set the variable.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app_tauri_lib::run();
|
||||||
|
}
|
||||||
84
crates/app-tauri/src/pty.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! Generic **PTY ↔ Tauri Channel** bridge infrastructure.
|
||||||
|
//!
|
||||||
|
//! ARCHITECTURE §2 decides that high-frequency PTY byte streams travel over
|
||||||
|
//! per-session [`tauri::ipc::Channel`]s rather than global events, for
|
||||||
|
//! throughput and isolation. This module provides the transport-side plumbing
|
||||||
|
//! that L3 will plug a real `PtyPort` into; here there is **no real PTY** yet —
|
||||||
|
//! only the registry + the abstraction that routes byte chunks to the right
|
||||||
|
//! frontend channel.
|
||||||
|
//!
|
||||||
|
//! Design:
|
||||||
|
//! - The frontend opens a terminal and passes a [`tauri::ipc::Channel`] for that
|
||||||
|
//! session. The backend registers it in [`PtyBridge`] keyed by `SessionId`.
|
||||||
|
//! - Whatever produces output (the PTY adapter in L3) calls
|
||||||
|
//! [`PtyBridge::send_output`], which forwards the bytes on the matching
|
||||||
|
//! channel. Bytes are sent as-is; the frontend xterm wrapper consumes them.
|
||||||
|
//! - [`PtyBridge::unregister`] tears the channel down on terminal close.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use tauri::ipc::Channel;
|
||||||
|
|
||||||
|
use domain::ids::SessionId;
|
||||||
|
|
||||||
|
/// A chunk of PTY output bytes destined for a specific session's channel.
|
||||||
|
///
|
||||||
|
/// Sent as a raw byte vector; serde encodes it for the IPC channel. Kept as a
|
||||||
|
/// distinct type so the wire shape can evolve (e.g. add a sequence number for
|
||||||
|
/// backpressure handling) without touching call sites.
|
||||||
|
pub type PtyChunk = Vec<u8>;
|
||||||
|
|
||||||
|
/// Registry mapping live terminal sessions to their output [`Channel`].
|
||||||
|
///
|
||||||
|
/// Thread-safe; cloned `Arc<PtyBridge>` is held in [`crate::state::AppState`].
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PtyBridge {
|
||||||
|
channels: Mutex<HashMap<SessionId, Channel<PtyChunk>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PtyBridge {
|
||||||
|
/// Creates an empty bridge.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
channels: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers the output channel for a session (called when a terminal is
|
||||||
|
/// opened, from a `#[tauri::command]` that receives the `Channel` argument).
|
||||||
|
pub fn register(&self, session: SessionId, channel: Channel<PtyChunk>) {
|
||||||
|
if let Ok(mut map) = self.channels.lock() {
|
||||||
|
map.insert(session, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a session's channel (terminal closed).
|
||||||
|
pub fn unregister(&self, session: &SessionId) {
|
||||||
|
if let Ok(mut map) = self.channels.lock() {
|
||||||
|
map.remove(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forwards a chunk of output bytes to a session's channel.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the chunk was delivered, `false` if no channel is
|
||||||
|
/// registered for the session (e.g. already closed). In L3 the PTY adapter's
|
||||||
|
/// output stream drives this.
|
||||||
|
pub fn send_output(&self, session: &SessionId, chunk: PtyChunk) -> bool {
|
||||||
|
let Ok(map) = self.channels.lock() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match map.get(session) {
|
||||||
|
Some(channel) => channel.send(chunk).is_ok(),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of currently-registered sessions (handy for tests/diagnostics).
|
||||||
|
#[must_use]
|
||||||
|
pub fn active_sessions(&self) -> usize {
|
||||||
|
self.channels.lock().map(|m| m.len()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
479
crates/app-tauri/src/state.rs
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
//! Managed application state — the product of the **composition root**.
|
||||||
|
//!
|
||||||
|
//! The composition root ([`build_app_state`]) is the *single* place that
|
||||||
|
//! constructs concrete adapters (`new ConcreteAdapter`) and injects them as
|
||||||
|
//! `Arc<dyn Port>` into the use cases (ARCHITECTURE §1.1, §10). The use cases
|
||||||
|
//! are then exposed through `tauri::State<AppState>` to the command handlers.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use application::{
|
||||||
|
CloseProject, CloseTab, CloseTerminal, ConfigureProfiles, CreateAgentFromScratch,
|
||||||
|
CreateAgentFromTemplate, CreateLayout, CreateProject, CreateTemplate, DeleteAgent,
|
||||||
|
DeleteLayout, DeleteProfile, DeleteTemplate, DetectAgentDrift, DetectProfiles, FirstRunState,
|
||||||
|
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
|
||||||
|
HealthUseCase, LaunchAgent, ListAgents, ListLayouts, ListProfiles, ListProjects, ListSkills,
|
||||||
|
ListTemplates, LoadLayout, MoveTabToNewWindow, MutateLayout, OpenProject, OpenTerminal,
|
||||||
|
ReadAgentContext, ReferenceProfiles, RenameLayout, ResizeTerminal, SaveProfile, SetActiveLayout,
|
||||||
|
AssignSkillToAgent, CreateSkill, DeleteSkill, UnassignSkillFromAgent, UpdateSkill,
|
||||||
|
SyncAgentWithTemplate, TerminalSessions, UpdateAgentContext, UpdateTemplate, WriteToTerminal,
|
||||||
|
};
|
||||||
|
use domain::ports::{
|
||||||
|
AgentContextStore, AgentRuntime, Clock, EventBus, FileSystem, GitPort, IdGenerator,
|
||||||
|
ProcessSpawner, ProfileStore, ProjectStore, PtyPort, SkillStore, TemplateStore,
|
||||||
|
};
|
||||||
|
|
||||||
|
use infrastructure::{
|
||||||
|
CliAgentRuntime, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore, Git2Repository,
|
||||||
|
IdeaiContextStore, LocalFileSystem, LocalProcessSpawner, PortablePtyAdapter, SystemClock,
|
||||||
|
TokioBroadcastEventBus, UuidGenerator,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::pty::PtyBridge;
|
||||||
|
|
||||||
|
/// Everything the IPC layer needs at runtime, managed by Tauri.
|
||||||
|
///
|
||||||
|
/// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete
|
||||||
|
/// adapters are owned here and never leak past the composition root as concrete
|
||||||
|
/// types — downstream code only sees the `Arc<dyn Port>` held inside the use
|
||||||
|
/// cases.
|
||||||
|
pub struct AppState {
|
||||||
|
/// Trivial health use case validating the end-to-end wiring.
|
||||||
|
pub health: Arc<HealthUseCase>,
|
||||||
|
/// Create a project (init `.ideai/`, register it).
|
||||||
|
pub create_project: Arc<CreateProject>,
|
||||||
|
/// Open a project (load meta + manifest).
|
||||||
|
pub open_project: Arc<OpenProject>,
|
||||||
|
/// Close a project (persist state).
|
||||||
|
pub close_project: Arc<CloseProject>,
|
||||||
|
/// Close a tab.
|
||||||
|
pub close_tab: Arc<CloseTab>,
|
||||||
|
/// List known projects.
|
||||||
|
pub list_projects: Arc<ListProjects>,
|
||||||
|
/// Open a terminal (spawn PTY, register session).
|
||||||
|
pub open_terminal: Arc<OpenTerminal>,
|
||||||
|
/// Write keystrokes to a terminal.
|
||||||
|
pub write_terminal: Arc<WriteToTerminal>,
|
||||||
|
/// Resize a terminal.
|
||||||
|
pub resize_terminal: Arc<ResizeTerminal>,
|
||||||
|
/// Close a terminal (kill PTY).
|
||||||
|
pub close_terminal: Arc<CloseTerminal>,
|
||||||
|
/// Load a project's persisted layout tree.
|
||||||
|
pub load_layout: Arc<LoadLayout>,
|
||||||
|
/// Mutate + persist a project's layout tree.
|
||||||
|
pub mutate_layout: Arc<MutateLayout>,
|
||||||
|
/// List all named layouts for a project (#4).
|
||||||
|
pub list_layouts: Arc<ListLayouts>,
|
||||||
|
/// Create a new named layout (#4).
|
||||||
|
pub create_layout: Arc<CreateLayout>,
|
||||||
|
/// Rename a named layout (#4).
|
||||||
|
pub rename_layout: Arc<RenameLayout>,
|
||||||
|
/// Delete a named layout (#4).
|
||||||
|
pub delete_layout: Arc<DeleteLayout>,
|
||||||
|
/// Set the active named layout (#4).
|
||||||
|
pub set_active_layout: Arc<SetActiveLayout>,
|
||||||
|
/// Detect which candidate profiles' CLIs are installed (first-run).
|
||||||
|
pub detect_profiles: Arc<DetectProfiles>,
|
||||||
|
/// List configured profiles.
|
||||||
|
pub list_profiles: Arc<ListProfiles>,
|
||||||
|
/// Save (upsert) a profile.
|
||||||
|
pub save_profile: Arc<SaveProfile>,
|
||||||
|
/// Delete a profile.
|
||||||
|
pub delete_profile: Arc<DeleteProfile>,
|
||||||
|
/// Persist the batch of chosen profiles (closes the first run).
|
||||||
|
pub configure_profiles: Arc<ConfigureProfiles>,
|
||||||
|
/// Expose the pre-filled reference catalogue.
|
||||||
|
pub reference_profiles: Arc<ReferenceProfiles>,
|
||||||
|
/// Whether the first-run wizard should show + the reference catalogue.
|
||||||
|
pub first_run_state: Arc<FirstRunState>,
|
||||||
|
/// The local PTY adapter, kept port-typed so the presentation layer can
|
||||||
|
/// `subscribe_output` to pump bytes into the [`PtyBridge`] (it owns transport).
|
||||||
|
pub pty_port: Arc<dyn PtyPort>,
|
||||||
|
/// Active-terminal registry shared by the terminal use cases.
|
||||||
|
pub terminal_sessions: Arc<TerminalSessions>,
|
||||||
|
/// The domain event bus (also handed to the event relay).
|
||||||
|
pub event_bus: Arc<TokioBroadcastEventBus>,
|
||||||
|
/// Generic PTY↔Channel bridge registry (consumed by L3).
|
||||||
|
pub pty_bridge: Arc<PtyBridge>,
|
||||||
|
// --- Agents (L6) ---
|
||||||
|
/// Create a project agent from scratch.
|
||||||
|
pub create_agent: Arc<CreateAgentFromScratch>,
|
||||||
|
/// List a project's agents.
|
||||||
|
pub list_agents: Arc<ListAgents>,
|
||||||
|
/// Read an agent's Markdown context.
|
||||||
|
pub read_agent_context: Arc<ReadAgentContext>,
|
||||||
|
/// Overwrite an agent's Markdown context.
|
||||||
|
pub update_agent_context: Arc<UpdateAgentContext>,
|
||||||
|
/// Delete an agent from the manifest.
|
||||||
|
pub delete_agent: Arc<DeleteAgent>,
|
||||||
|
/// Launch an agent (spawn PTY, apply injection strategy).
|
||||||
|
pub launch_agent: Arc<LaunchAgent>,
|
||||||
|
/// Project registry — used by agent commands to resolve a `Project` from an id.
|
||||||
|
pub project_store: Arc<dyn ProjectStore>,
|
||||||
|
// --- Windows (L10) ---
|
||||||
|
/// Detach a tab into a new OS window (persists the workspace topology).
|
||||||
|
pub move_tab: Arc<MoveTabToNewWindow>,
|
||||||
|
// --- Templates & sync (L7) ---
|
||||||
|
/// Create a template in the global store.
|
||||||
|
pub create_template: Arc<CreateTemplate>,
|
||||||
|
/// Update a template's content (bumps version).
|
||||||
|
pub update_template: Arc<UpdateTemplate>,
|
||||||
|
/// List all templates in the global store.
|
||||||
|
pub list_templates: Arc<ListTemplates>,
|
||||||
|
/// Delete a template from the global store.
|
||||||
|
pub delete_template: Arc<DeleteTemplate>,
|
||||||
|
/// Create an agent from a template.
|
||||||
|
pub create_agent_from_template: Arc<CreateAgentFromTemplate>,
|
||||||
|
/// Detect which synchronized agents are behind their template.
|
||||||
|
pub detect_agent_drift: Arc<DetectAgentDrift>,
|
||||||
|
/// Apply a template update to a synchronized agent.
|
||||||
|
pub sync_agent_with_template: Arc<SyncAgentWithTemplate>,
|
||||||
|
// --- Git (L8) ---
|
||||||
|
/// Report the working-tree status of a repository.
|
||||||
|
pub git_status: Arc<GitStatus>,
|
||||||
|
/// Stage a path.
|
||||||
|
pub git_stage: Arc<GitStage>,
|
||||||
|
/// Unstage a path.
|
||||||
|
pub git_unstage: Arc<GitUnstage>,
|
||||||
|
/// Create a commit.
|
||||||
|
pub git_commit: Arc<GitCommit>,
|
||||||
|
/// List branches.
|
||||||
|
pub git_branches: Arc<GitBranches>,
|
||||||
|
/// Check out a branch.
|
||||||
|
pub git_checkout: Arc<GitCheckout>,
|
||||||
|
/// Return the recent commit log.
|
||||||
|
pub git_log: Arc<GitLog>,
|
||||||
|
/// Initialise a repository.
|
||||||
|
pub git_init: Arc<GitInit>,
|
||||||
|
/// Return the commit graph for all local branches.
|
||||||
|
pub git_graph: Arc<GitGraph>,
|
||||||
|
// --- Skills (L12) ---
|
||||||
|
/// Create a skill in a scope's store.
|
||||||
|
pub create_skill: Arc<CreateSkill>,
|
||||||
|
/// Update a skill's content.
|
||||||
|
pub update_skill: Arc<UpdateSkill>,
|
||||||
|
/// List skills in a scope.
|
||||||
|
pub list_skills: Arc<ListSkills>,
|
||||||
|
/// Delete a skill from its scope's store.
|
||||||
|
pub delete_skill: Arc<DeleteSkill>,
|
||||||
|
/// Assign a skill to an agent (records a `SkillRef`).
|
||||||
|
pub assign_skill: Arc<AssignSkillToAgent>,
|
||||||
|
/// Unassign a skill from an agent.
|
||||||
|
pub unassign_skill: Arc<UnassignSkillFromAgent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
/// **Composition root.** Builds all adapters and use cases.
|
||||||
|
///
|
||||||
|
/// `app_data_dir` is the machine-local IDE data directory (ARCHITECTURE
|
||||||
|
/// §9.2), resolved by the caller via the Tauri path API and injected here so
|
||||||
|
/// the stores never touch Tauri themselves (Dependency Inversion).
|
||||||
|
///
|
||||||
|
/// This is the only function that constructs concrete adapters; every other
|
||||||
|
/// layer depends on ports. Adapters added in later lots (PTY, git, remote)
|
||||||
|
/// are wired in here.
|
||||||
|
#[must_use]
|
||||||
|
pub fn build(app_data_dir: PathBuf) -> Self {
|
||||||
|
// --- Concrete adapters (driven adapters) ---
|
||||||
|
let event_bus = Arc::new(TokioBroadcastEventBus::new());
|
||||||
|
let clock = Arc::new(SystemClock::new());
|
||||||
|
let ids = Arc::new(UuidGenerator::new());
|
||||||
|
let fs = Arc::new(LocalFileSystem::new());
|
||||||
|
let store = Arc::new(FsProjectStore::new(
|
||||||
|
Arc::clone(&fs) as Arc<dyn FileSystem>,
|
||||||
|
app_data_dir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Port-typed handles for injection.
|
||||||
|
let fs_port = Arc::clone(&fs) as Arc<dyn FileSystem>;
|
||||||
|
let store_port = Arc::clone(&store) as Arc<dyn ProjectStore>;
|
||||||
|
let events_port = Arc::clone(&event_bus) as Arc<dyn EventBus>;
|
||||||
|
|
||||||
|
// --- Use cases (ports injected as Arc<dyn Port>) ---
|
||||||
|
let health = Arc::new(HealthUseCase::new(
|
||||||
|
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
|
||||||
|
let create_project = Arc::new(CreateProject::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let open_project = Arc::new(OpenProject::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
));
|
||||||
|
let close_project = Arc::new(CloseProject::new(Arc::clone(&store_port)));
|
||||||
|
let close_tab = Arc::new(CloseTab::new(Arc::clone(&store_port)));
|
||||||
|
let list_projects = Arc::new(ListProjects::new(Arc::clone(&store_port)));
|
||||||
|
|
||||||
|
// --- PTY adapter + terminal use cases (L3) ---
|
||||||
|
let pty = Arc::new(PortablePtyAdapter::new());
|
||||||
|
let pty_port = Arc::clone(&pty) as Arc<dyn PtyPort>;
|
||||||
|
let terminal_sessions = Arc::new(TerminalSessions::new());
|
||||||
|
|
||||||
|
let open_terminal = Arc::new(OpenTerminal::new(
|
||||||
|
Arc::clone(&pty_port),
|
||||||
|
Arc::clone(&terminal_sessions),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let write_terminal = Arc::new(WriteToTerminal::new(
|
||||||
|
Arc::clone(&pty_port),
|
||||||
|
Arc::clone(&terminal_sessions),
|
||||||
|
));
|
||||||
|
let resize_terminal = Arc::new(ResizeTerminal::new(
|
||||||
|
Arc::clone(&pty_port),
|
||||||
|
Arc::clone(&terminal_sessions),
|
||||||
|
));
|
||||||
|
let close_terminal = Arc::new(CloseTerminal::new(
|
||||||
|
Arc::clone(&pty_port),
|
||||||
|
Arc::clone(&terminal_sessions),
|
||||||
|
));
|
||||||
|
|
||||||
|
// --- Layout use cases (L4 + #4) ---
|
||||||
|
let load_layout = Arc::new(LoadLayout::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
));
|
||||||
|
let mutate_layout = Arc::new(MutateLayout::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let list_layouts = Arc::new(ListLayouts::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
));
|
||||||
|
let create_layout = Arc::new(CreateLayout::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let rename_layout = Arc::new(RenameLayout::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let delete_layout = Arc::new(DeleteLayout::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let set_active_layout = Arc::new(SetActiveLayout::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
|
||||||
|
// --- Profiles & AI runtime (L5) ---
|
||||||
|
// One generic, profile-driven runtime adapter (Open/Closed): it holds the
|
||||||
|
// process spawner used for detection. The profile store persists
|
||||||
|
// `profiles.json` in the same machine-local app-data dir as the project
|
||||||
|
// registry.
|
||||||
|
let spawner = Arc::new(LocalProcessSpawner::new());
|
||||||
|
let spawner_port = Arc::clone(&spawner) as Arc<dyn ProcessSpawner>;
|
||||||
|
let runtime = Arc::new(CliAgentRuntime::new(Arc::clone(&spawner_port)));
|
||||||
|
let runtime_port = Arc::clone(&runtime) as Arc<dyn AgentRuntime>;
|
||||||
|
|
||||||
|
let profile_store = Arc::new(FsProfileStore::new(
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
app_data_dir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
|
let profile_store_port = Arc::clone(&profile_store) as Arc<dyn ProfileStore>;
|
||||||
|
|
||||||
|
let detect_profiles = Arc::new(DetectProfiles::new(Arc::clone(&runtime_port)));
|
||||||
|
let list_profiles = Arc::new(ListProfiles::new(Arc::clone(&profile_store_port)));
|
||||||
|
let save_profile = Arc::new(SaveProfile::new(Arc::clone(&profile_store_port)));
|
||||||
|
let delete_profile = Arc::new(DeleteProfile::new(Arc::clone(&profile_store_port)));
|
||||||
|
let configure_profiles = Arc::new(ConfigureProfiles::new(Arc::clone(&profile_store_port)));
|
||||||
|
let reference_profiles = Arc::new(ReferenceProfiles::new());
|
||||||
|
let first_run_state = Arc::new(FirstRunState::new(Arc::clone(&profile_store_port)));
|
||||||
|
|
||||||
|
let pty_bridge = Arc::new(PtyBridge::new());
|
||||||
|
|
||||||
|
// --- Agent context store + use cases (L6) ---
|
||||||
|
let contexts = Arc::new(IdeaiContextStore::new(Arc::clone(&fs_port)));
|
||||||
|
let contexts_port = Arc::clone(&contexts) as Arc<dyn AgentContextStore>;
|
||||||
|
|
||||||
|
// --- Skill store (L12) ---
|
||||||
|
// Global skills live in the machine-local app-data dir; project skills are
|
||||||
|
// resolved per call from each project's `.ideai/` (so one store serves all
|
||||||
|
// open projects). Shared by the skill use cases and the agent launcher
|
||||||
|
// (assigned-skill injection into the convention file, §14.2).
|
||||||
|
let skill_store = Arc::new(FsSkillStore::new(
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
app_data_dir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
|
let skill_store_port = Arc::clone(&skill_store) as Arc<dyn SkillStore>;
|
||||||
|
|
||||||
|
let create_agent = Arc::new(CreateAgentFromScratch::new(
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let list_agents = Arc::new(ListAgents::new(Arc::clone(&contexts_port)));
|
||||||
|
let read_agent_context = Arc::new(ReadAgentContext::new(Arc::clone(&contexts_port)));
|
||||||
|
let update_agent_context = Arc::new(UpdateAgentContext::new(Arc::clone(&contexts_port)));
|
||||||
|
let delete_agent = Arc::new(DeleteAgent::new(
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
// LaunchAgent shares the SAME pty_port and terminal_sessions as the terminal
|
||||||
|
// use cases — indispensable for the PtyBridge to work correctly.
|
||||||
|
let launch_agent = Arc::new(LaunchAgent::new(
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&profile_store_port),
|
||||||
|
Arc::clone(&runtime_port),
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
Arc::clone(&pty_port),
|
||||||
|
Arc::clone(&skill_store_port),
|
||||||
|
Arc::clone(&terminal_sessions),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
|
||||||
|
let project_store = Arc::clone(&store_port);
|
||||||
|
|
||||||
|
// --- Template store + use cases (L7) ---
|
||||||
|
let template_store = Arc::new(FsTemplateStore::new(
|
||||||
|
Arc::clone(&fs_port),
|
||||||
|
app_data_dir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
|
let template_store_port = Arc::clone(&template_store) as Arc<dyn TemplateStore>;
|
||||||
|
|
||||||
|
let create_template = Arc::new(CreateTemplate::new(
|
||||||
|
Arc::clone(&template_store_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
));
|
||||||
|
let update_template = Arc::new(UpdateTemplate::new(
|
||||||
|
Arc::clone(&template_store_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let list_templates = Arc::new(ListTemplates::new(Arc::clone(&template_store_port)));
|
||||||
|
let delete_template = Arc::new(DeleteTemplate::new(Arc::clone(&template_store_port)));
|
||||||
|
let create_agent_from_template = Arc::new(CreateAgentFromTemplate::new(
|
||||||
|
Arc::clone(&template_store_port),
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let detect_agent_drift = Arc::new(DetectAgentDrift::new(
|
||||||
|
Arc::clone(&template_store_port),
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let sync_agent_with_template = Arc::new(SyncAgentWithTemplate::new(
|
||||||
|
Arc::clone(&template_store_port),
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
|
||||||
|
// --- Git adapter + use cases (L8) ---
|
||||||
|
let git = Arc::new(Git2Repository::new());
|
||||||
|
let git_port = Arc::clone(&git) as Arc<dyn GitPort>;
|
||||||
|
|
||||||
|
let git_status = Arc::new(GitStatus::new(Arc::clone(&git_port)));
|
||||||
|
let git_stage = Arc::new(GitStage::new(Arc::clone(&git_port)));
|
||||||
|
let git_unstage = Arc::new(GitUnstage::new(Arc::clone(&git_port)));
|
||||||
|
let git_commit = Arc::new(GitCommit::new(Arc::clone(&git_port), Arc::clone(&events_port)));
|
||||||
|
let git_branches = Arc::new(GitBranches::new(Arc::clone(&git_port)));
|
||||||
|
let git_checkout = Arc::new(GitCheckout::new(
|
||||||
|
Arc::clone(&git_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let git_log = Arc::new(GitLog::new(Arc::clone(&git_port)));
|
||||||
|
let git_init = Arc::new(GitInit::new(Arc::clone(&git_port), Arc::clone(&events_port)));
|
||||||
|
let git_graph = Arc::new(GitGraph::new(Arc::clone(&git_port)));
|
||||||
|
|
||||||
|
// --- Skill use cases (L12) ---
|
||||||
|
// Reuse the skill store (built above for the launcher) and the shared
|
||||||
|
// agent context store for the agent↔skill assignment.
|
||||||
|
let create_skill = Arc::new(CreateSkill::new(
|
||||||
|
Arc::clone(&skill_store_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
));
|
||||||
|
let update_skill = Arc::new(UpdateSkill::new(Arc::clone(&skill_store_port)));
|
||||||
|
let list_skills = Arc::new(ListSkills::new(Arc::clone(&skill_store_port)));
|
||||||
|
let delete_skill = Arc::new(DeleteSkill::new(Arc::clone(&skill_store_port)));
|
||||||
|
let assign_skill = Arc::new(AssignSkillToAgent::new(
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
let unassign_skill = Arc::new(UnassignSkillFromAgent::new(
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&events_port),
|
||||||
|
));
|
||||||
|
|
||||||
|
// --- Windows (L10) ---
|
||||||
|
let move_tab = Arc::new(MoveTabToNewWindow::new(
|
||||||
|
Arc::clone(&store_port),
|
||||||
|
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||||
|
));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
health,
|
||||||
|
create_project,
|
||||||
|
open_project,
|
||||||
|
close_project,
|
||||||
|
close_tab,
|
||||||
|
list_projects,
|
||||||
|
open_terminal,
|
||||||
|
write_terminal,
|
||||||
|
resize_terminal,
|
||||||
|
close_terminal,
|
||||||
|
load_layout,
|
||||||
|
mutate_layout,
|
||||||
|
list_layouts,
|
||||||
|
create_layout,
|
||||||
|
rename_layout,
|
||||||
|
delete_layout,
|
||||||
|
set_active_layout,
|
||||||
|
detect_profiles,
|
||||||
|
list_profiles,
|
||||||
|
save_profile,
|
||||||
|
delete_profile,
|
||||||
|
configure_profiles,
|
||||||
|
reference_profiles,
|
||||||
|
first_run_state,
|
||||||
|
pty_port,
|
||||||
|
terminal_sessions,
|
||||||
|
event_bus,
|
||||||
|
pty_bridge,
|
||||||
|
create_agent,
|
||||||
|
list_agents,
|
||||||
|
read_agent_context,
|
||||||
|
update_agent_context,
|
||||||
|
delete_agent,
|
||||||
|
launch_agent,
|
||||||
|
project_store,
|
||||||
|
create_template,
|
||||||
|
update_template,
|
||||||
|
list_templates,
|
||||||
|
delete_template,
|
||||||
|
create_agent_from_template,
|
||||||
|
detect_agent_drift,
|
||||||
|
sync_agent_with_template,
|
||||||
|
git_status,
|
||||||
|
git_stage,
|
||||||
|
git_unstage,
|
||||||
|
git_commit,
|
||||||
|
git_branches,
|
||||||
|
git_checkout,
|
||||||
|
git_log,
|
||||||
|
git_init,
|
||||||
|
git_graph,
|
||||||
|
create_skill,
|
||||||
|
update_skill,
|
||||||
|
list_skills,
|
||||||
|
delete_skill,
|
||||||
|
assign_skill,
|
||||||
|
unassign_skill,
|
||||||
|
move_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
crates/app-tauri/tauri.conf.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "IdeA",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "app.idea.ide",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../../frontend/dist",
|
||||||
|
"devUrl": "http://localhost:5173",
|
||||||
|
"beforeDevCommand": "npm --prefix ../frontend run dev"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "IdeA",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"resizable": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["appimage", "nsis"],
|
||||||
|
"icon": ["icons/icon.png"]
|
||||||
|
}
|
||||||
|
}
|
||||||
336
crates/app-tauri/tests/dto.rs
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
//! L1 tests for the IPC DTO (de)serialisation contract: camelCase on the wire,
|
||||||
|
//! stable `ErrorDto.code`, and the `DomainEvent -> DomainEventDto` mapping with
|
||||||
|
//! its tagged, camelCase JSON shape.
|
||||||
|
|
||||||
|
use app_tauri_lib::dto::{
|
||||||
|
parse_node_id, parse_session_id, ErrorDto, HealthRequestDto, HealthResponseDto, LayoutDto,
|
||||||
|
LayoutOperationDto, OpenTerminalRequestDto, ReattachResultDto, ResizeTerminalRequestDto,
|
||||||
|
TerminalClosedDto, WriteTerminalRequestDto,
|
||||||
|
};
|
||||||
|
use application::{CloseTerminalOutput, LayoutOperation, LoadLayoutOutput, OpenTerminalInput};
|
||||||
|
use domain::{Direction, LayoutNode, LayoutTree, LeafCell, NodeId};
|
||||||
|
use app_tauri_lib::events::{DomainEventDto, DOMAIN_EVENT};
|
||||||
|
|
||||||
|
use application::{AppError, HealthInput};
|
||||||
|
use domain::events::DomainEvent;
|
||||||
|
use domain::ids::{AgentId, SessionId};
|
||||||
|
use domain::ProjectId;
|
||||||
|
use domain::TemplateVersion;
|
||||||
|
use domain::TemplateId;
|
||||||
|
use serde_json::json;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn health_response_serialises_camel_case() {
|
||||||
|
let dto = HealthResponseDto {
|
||||||
|
version: "1.2.3".into(),
|
||||||
|
alive: true,
|
||||||
|
time_millis: 42,
|
||||||
|
correlation_id: "abc".into(),
|
||||||
|
note: Some("hi".into()),
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v,
|
||||||
|
json!({
|
||||||
|
"version": "1.2.3",
|
||||||
|
"alive": true,
|
||||||
|
"timeMillis": 42,
|
||||||
|
"correlationId": "abc",
|
||||||
|
"note": "hi",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn health_request_deserialises_camel_case_and_defaults() {
|
||||||
|
let dto: HealthRequestDto = serde_json::from_value(json!({ "note": "x" })).unwrap();
|
||||||
|
assert_eq!(HealthInput::from(dto).note.as_deref(), Some("x"));
|
||||||
|
|
||||||
|
// Missing note defaults to None.
|
||||||
|
let empty: HealthRequestDto = serde_json::from_value(json!({})).unwrap();
|
||||||
|
assert_eq!(HealthInput::from(empty).note, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_dto_carries_stable_code_and_message() {
|
||||||
|
let dto = ErrorDto::from(AppError::NotFound("project".into()));
|
||||||
|
assert_eq!(dto.code, "NOT_FOUND");
|
||||||
|
assert!(dto.message.contains("project"));
|
||||||
|
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v["code"], "NOT_FOUND");
|
||||||
|
assert!(v.get("message").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_event_relay_name_is_frozen() {
|
||||||
|
assert_eq!(DOMAIN_EVENT, "domain://event");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_event_dto_is_tagged_and_camel_case() {
|
||||||
|
let pid = ProjectId::from_uuid(Uuid::nil());
|
||||||
|
let dto = DomainEventDto::from(&DomainEvent::ProjectCreated { project_id: pid });
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v,
|
||||||
|
json!({ "type": "projectCreated", "projectId": pid.to_string() })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_event_dto_maps_agent_launched() {
|
||||||
|
let aid = AgentId::from_uuid(Uuid::nil());
|
||||||
|
let sid = SessionId::from_uuid(Uuid::nil());
|
||||||
|
let dto = DomainEventDto::from(&DomainEvent::AgentLaunched {
|
||||||
|
agent_id: aid,
|
||||||
|
session_id: sid,
|
||||||
|
});
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v["type"], "agentLaunched");
|
||||||
|
assert_eq!(v["agentId"], aid.to_string());
|
||||||
|
assert_eq!(v["sessionId"], sid.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_event_dto_maps_template_updated_version() {
|
||||||
|
let tid = TemplateId::from_uuid(Uuid::nil());
|
||||||
|
let dto = DomainEventDto::from(&DomainEvent::TemplateUpdated {
|
||||||
|
template_id: tid,
|
||||||
|
version: TemplateVersion::INITIAL,
|
||||||
|
});
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v["type"], "templateUpdated");
|
||||||
|
assert_eq!(v["templateId"], tid.to_string());
|
||||||
|
assert!(v["version"].is_u64());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_event_dto_maps_pty_output_bytes() {
|
||||||
|
let sid = SessionId::from_uuid(Uuid::nil());
|
||||||
|
let dto = DomainEventDto::from(&DomainEvent::PtyOutput {
|
||||||
|
session_id: sid,
|
||||||
|
bytes: vec![1, 2, 3],
|
||||||
|
});
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v["type"], "ptyOutput");
|
||||||
|
assert_eq!(v["bytes"], json!([1, 2, 3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Terminal DTOs (L3)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_terminal_request_deserialises_camel_case_with_defaults() {
|
||||||
|
// Minimal payload: command/args omitted → None / empty.
|
||||||
|
let dto: OpenTerminalRequestDto =
|
||||||
|
serde_json::from_value(json!({ "cwd": "/p", "rows": 24, "cols": 80 })).unwrap();
|
||||||
|
let input = OpenTerminalInput::from(dto);
|
||||||
|
assert_eq!(input.cwd, "/p");
|
||||||
|
assert_eq!(input.rows, 24);
|
||||||
|
assert_eq!(input.cols, 80);
|
||||||
|
assert_eq!(input.command, None);
|
||||||
|
assert!(input.args.is_empty());
|
||||||
|
assert_eq!(input.node_id, None);
|
||||||
|
|
||||||
|
// Full payload with explicit command + args.
|
||||||
|
let full: OpenTerminalRequestDto = serde_json::from_value(json!({
|
||||||
|
"cwd": "/q", "rows": 10, "cols": 20,
|
||||||
|
"command": "/bin/zsh", "args": ["-l"],
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let input = OpenTerminalInput::from(full);
|
||||||
|
assert_eq!(input.command.as_deref(), Some("/bin/zsh"));
|
||||||
|
assert_eq!(input.args, vec!["-l".to_owned()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_terminal_request_deserialises_session_id_and_data() {
|
||||||
|
let sid = SessionId::from_uuid(Uuid::nil());
|
||||||
|
let dto: WriteTerminalRequestDto = serde_json::from_value(json!({
|
||||||
|
"sessionId": sid.to_string(),
|
||||||
|
"data": [104, 105],
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let input = dto.into_input().expect("valid session id");
|
||||||
|
assert_eq!(input.session_id, sid);
|
||||||
|
assert_eq!(input.data, vec![104u8, 105]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_terminal_request_rejects_bad_session_id() {
|
||||||
|
let dto: WriteTerminalRequestDto =
|
||||||
|
serde_json::from_value(json!({ "sessionId": "not-a-uuid", "data": [] })).unwrap();
|
||||||
|
let err = dto.into_input().expect_err("malformed id rejected");
|
||||||
|
assert_eq!(err.code, "INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_terminal_request_deserialises_camel_case() {
|
||||||
|
let sid = SessionId::from_uuid(Uuid::nil());
|
||||||
|
let dto: ResizeTerminalRequestDto = serde_json::from_value(json!({
|
||||||
|
"sessionId": sid.to_string(), "rows": 40, "cols": 120,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let input = dto.into_input().expect("valid");
|
||||||
|
assert_eq!(input.session_id, sid);
|
||||||
|
assert_eq!(input.rows, 40);
|
||||||
|
assert_eq!(input.cols, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_closed_dto_serialises_code_camel_case() {
|
||||||
|
let dto = TerminalClosedDto::from(CloseTerminalOutput { code: Some(0) });
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v, json!({ "code": 0 }));
|
||||||
|
|
||||||
|
// Signalled (None) round-trips as null.
|
||||||
|
let none = TerminalClosedDto::from(CloseTerminalOutput { code: None });
|
||||||
|
assert_eq!(serde_json::to_value(&none).unwrap(), json!({ "code": null }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reattach_result_dto_serialises_camel_case() {
|
||||||
|
let dto = ReattachResultDto {
|
||||||
|
session_id: "sess-1".into(),
|
||||||
|
scrollback: vec![104, 105],
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v, json!({ "sessionId": "sess-1", "scrollback": [104, 105] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_session_id_accepts_uuid_and_rejects_garbage() {
|
||||||
|
let sid = SessionId::from_uuid(Uuid::nil());
|
||||||
|
assert_eq!(parse_session_id(&sid.to_string()).unwrap(), sid);
|
||||||
|
assert_eq!(parse_session_id("nope").unwrap_err().code, "INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Layout (L4)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn nid(n: u128) -> NodeId {
|
||||||
|
NodeId::from_uuid(Uuid::from_u128(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_dto_serialises_camelcase_tagged_tree() {
|
||||||
|
// A single-leaf tree → transparent over LayoutTree, tagged `{type,node}`.
|
||||||
|
let tree = LayoutTree::single(LeafCell {
|
||||||
|
id: nid(1),
|
||||||
|
session: None,
|
||||||
|
agent: None,
|
||||||
|
});
|
||||||
|
let dto = LayoutDto::from(LoadLayoutOutput {
|
||||||
|
layout_id: domain::LayoutId::new_random(),
|
||||||
|
layout: tree,
|
||||||
|
});
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v["root"]["type"], "leaf", "enum tagged on `type`");
|
||||||
|
assert_eq!(v["root"]["node"]["id"], nid(1).to_string());
|
||||||
|
// Empty session is skipped (skip_serializing_if).
|
||||||
|
assert!(v["root"]["node"].get("session").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_operation_dto_split_deserialises_camelcase() {
|
||||||
|
let json = json!({
|
||||||
|
"type": "split",
|
||||||
|
"target": nid(1).to_string(),
|
||||||
|
"direction": "row",
|
||||||
|
"newLeaf": nid(2).to_string(),
|
||||||
|
"container": nid(9).to_string(),
|
||||||
|
});
|
||||||
|
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||||
|
match dto.into_operation().unwrap() {
|
||||||
|
LayoutOperation::Split {
|
||||||
|
target,
|
||||||
|
direction,
|
||||||
|
new_leaf,
|
||||||
|
container,
|
||||||
|
} => {
|
||||||
|
assert_eq!(target, nid(1));
|
||||||
|
assert_eq!(direction, Direction::Row);
|
||||||
|
assert_eq!(new_leaf, nid(2));
|
||||||
|
assert_eq!(container, nid(9));
|
||||||
|
}
|
||||||
|
other => panic!("expected Split, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_operation_dto_set_session_accepts_null_session() {
|
||||||
|
// `session: null` → detach (None).
|
||||||
|
let json = json!({
|
||||||
|
"type": "setSession",
|
||||||
|
"target": nid(1).to_string(),
|
||||||
|
"session": null,
|
||||||
|
});
|
||||||
|
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||||
|
match dto.into_operation().unwrap() {
|
||||||
|
LayoutOperation::SetSession { target, session } => {
|
||||||
|
assert_eq!(target, nid(1));
|
||||||
|
assert!(session.is_none());
|
||||||
|
}
|
||||||
|
other => panic!("expected SetSession, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_operation_dto_resize_carries_weights() {
|
||||||
|
let json = json!({
|
||||||
|
"type": "resize",
|
||||||
|
"container": nid(9).to_string(),
|
||||||
|
"weights": [3.0, 1.0],
|
||||||
|
});
|
||||||
|
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||||
|
match dto.into_operation().unwrap() {
|
||||||
|
LayoutOperation::Resize { container, weights } => {
|
||||||
|
assert_eq!(container, nid(9));
|
||||||
|
assert_eq!(weights, vec![3.0, 1.0]);
|
||||||
|
}
|
||||||
|
other => panic!("expected Resize, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_operation_dto_rejects_malformed_uuid_as_invalid() {
|
||||||
|
let json = json!({
|
||||||
|
"type": "merge",
|
||||||
|
"container": "not-a-uuid",
|
||||||
|
"keepIndex": 0,
|
||||||
|
});
|
||||||
|
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||||
|
let err = dto.into_operation().expect_err("malformed uuid rejected");
|
||||||
|
assert_eq!(err.code, "INVALID", "got {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_node_id_accepts_uuid_and_rejects_garbage() {
|
||||||
|
assert_eq!(parse_node_id(&nid(7).to_string()).unwrap(), nid(7));
|
||||||
|
assert_eq!(parse_node_id("garbage").unwrap_err().code, "INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_dto_round_trips_a_split_tree_shape() {
|
||||||
|
// Build a split tree, serialise via the DTO, and confirm the tagged shape
|
||||||
|
// re-parses into an equivalent LayoutTree.
|
||||||
|
let tree = LayoutTree::single(LeafCell {
|
||||||
|
id: nid(1),
|
||||||
|
session: None,
|
||||||
|
agent: None,
|
||||||
|
})
|
||||||
|
.split(nid(1), Direction::Column, LeafCell { id: nid(2), session: None, agent: None }, nid(9))
|
||||||
|
.unwrap();
|
||||||
|
let dto = LayoutDto::from(LoadLayoutOutput {
|
||||||
|
layout_id: domain::LayoutId::new_random(),
|
||||||
|
layout: tree.clone(),
|
||||||
|
});
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
let back: LayoutTree = serde_json::from_value(v).unwrap();
|
||||||
|
assert_eq!(back, tree, "DTO serialisation round-trips the tree");
|
||||||
|
assert!(matches!(back.root, LayoutNode::Split(_)));
|
||||||
|
}
|
||||||