Files
IdeA/ARCHITECTURE.md

47 KiB

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 == trueorigin == 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_mdversion + 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 :
    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 :
    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 :
    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 :
    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 :
    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, RemoteHostPtyPort/FileSystem, EventBus
OpenTerminal Ouvre un PTY simple dans une cellule. RemoteHostPtyPort, 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.

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 :

{
  "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 :

{ "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.