# Architecture — explication ## De quoi s'agit-il ? Ori transpose dans le navigateur l'expérience d'un panel d'agent d'éditeur — celui de Zed, en l'occurrence. La contrainte intéressante est que l'[Agent Client Protocol](https://agentclientprotocol.com) a été conçu pour des éditeurs : un client lance l'agent comme sous-processus et lui parle en JSON-RPC sur son stdio. Un navigateur ne peut pas lancer de processus ; il faut donc quelque chose au milieu. Ce quelque chose, c'est le backend Go d'Ori. ``` navigateur ⇆ WebSocket ⇆ backend Go ⇆ stdio (ACP / JSON-RPC) ⇆ agent (claude-code-acp, …) ``` Le diagramme de dépendances des packages est maintenu dans [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (à ouvrir avec diagrams.net ou l'extension Draw.io de VS Code). ## Les pièces - **`ui/` (SPA React)** — affiche la conversation : markdown streamé, réflexions repliables, cartes de tool calls avec statuts et diffs, plan de l'agent, demandes de permission. Tout son état dérive des événements serveur pliés par un reducer pur, ce qui rend l'interface testable sans navigateur ni réseau. - **`internal/httpserver`** — sert la SPA (embarquée dans le binaire via `go:embed`, si bien qu'Ori se distribue en un seul fichier) et monte les routes supplémentaires comme `/ws`. - **`internal/bridge`** — le cœur du backend. Il traduit entre deux mondes asynchrones : l'agent streame ses updates et bloque sur ses demandes de permission, pendant que les navigateurs se connectent, se déconnectent et répondent à vitesse humaine. Il diffuse les événements, rejoue l'historique aux retardataires, et route chaque décision de permission vers l'appel JSON-RPC exact qui l'attend. - **`internal/agent`** — lance le processus de l'agent et pilote le cycle de vie de la session ACP (initialize, session/new, prompt, cancel) via le client [`coder/acp-go-sdk`](https://github.com/coder/acp-go-sdk). Il est agnostique de l'agent : tout ce qui parle ACP sur stdio se branche. - **`internal/mockagent` + `cmd/ori-mock-agent`** — un agent ACP déterministe utilisé par le test de bout en bout et par `make run-mock`, pour exercer toute l'application sans agent IA, sans compte et sans réseau. - **`internal/skills`** — découvre les skills Claude Code (frontmatter des `SKILL.md`) dans le projet et le répertoire personnel, pour le sélecteur `/` du composer. - **`internal/files`** — l'API fichiers du workspace (lister, chercher, lire, octets bruts, écrire) derrière l'arborescence, la preview et l'éditeur de la SPA. L'accès est volontairement non restreint : ori vise des sandboxes déjà isolées, et l'agent a de toute façon la même portée. - **`internal/terminal`** — un vrai shell par connexion WebSocket, dans un pseudo-terminal (`creack/pty`), piloté par xterm.js dans le navigateur. ## Mentions, skills et le prompt ACP Le sélecteur `@` du composer posait une question de conception : comment un fichier parvient-il à l'agent ? ACP a déjà la réponse — tout agent doit accepter des blocs de contenu `resource_link` dans un prompt — donc le navigateur envoie le texte plus une petite liste `attachments`, et le bridge convertit chacune en `resource_link` (URI `file://`) à côté du bloc texte. La mention `@chemin` reste visible dans le texte, si bien que la conversation se lit naturellement dans tous les clients et dans les rejeux, tandis que le lien structuré permet à l'agent d'ouvrir le fichier sans deviner. Le sélecteur `/` n'a demandé aucun protocole : Claude Code invoque un skill ou une commande slash à partir du texte `/nom …`, donc la liste ne fait qu'insérer du texte, en fusionnant les skills trouvés sur disque et les commandes que l'agent annonce via `available_commands_update`. ## Previews : images et draw.io Les images sont diffusées par `GET /api/raw` et rendues par le navigateur lui-même, à la fois le moteur le plus simple et le plus capable disponible. Les diagrammes draw.io sont la seule preview qui s'écarte de la règle « embarqué, pas de CDN » fixée par Monaco : il n'existe pas de moteur de rendu hors ligne qu'ori pourrait livrer — le `viewer.min.js` de draw.io n'est pas publié sur npm, le paquet archivé `mxgraph` n'a pas la bibliothèque de formes de draw.io, et le seul convertisseur navigateur sur npm (`@markdown-viewer/drawio2svg`) est sous licence GPL-3.0 uniquement. Le panneau embarque donc `embed.diagrams.net` dans une iframe et lui transmet le XML par son protocole postMessage — le fichier ne quitte pas le navigateur — et se replie sur un avertissement plus la source XML quand l'hôte est injoignable, comme il peut l'être dans une sandbox isolée. Vendoriser `viewer-static.min.js` depuis le dépôt draw.io sous Apache-2.0 (environ 3 Mo) reste la voie vers un rendu hors ligne si ce compromis change un jour. ## Le panneau workspace La SPA est passée d'une colonne de chat unique à chat-plus-workspace : une arborescence de fichiers et trois onglets (Preview, Editor, Terminal). Trois choix la structurent. Monaco sert à la fois la preview de code en lecture seule et l'éditeur — une seule grosse dépendance au lieu de deux piles de colorisation — et il est embarqué avec ses workers, rien ne se charge depuis un CDN ; il est chargé à la demande, ce qui garde le bundle initial léger. Le markdown se rend via le même composant que les messages du chat, et l'AsciiDoc via un Asciidoctor importé paresseusement. Enfin, les panneaux inactifs sont cachés plutôt que démontés : la session shell du terminal et le tampon non sauvé de l'éditeur survivent aux changements d'onglet et au repli du panneau. La colonne de l'arborescence est redimensionnable et sa largeur suit la même règle « le store possède l'état » que le reste du panneau : elle vit dans le store workspace, atteint la feuille de style par une propriété CSS personnalisée (`--filetree-width`) posée sur le panneau, et est persistée en localStorage — une commodité par navigateur, pas un état du serveur, qui reste donc volontairement hors du protocole WebSocket. Le thème de couleurs est un état de même nature. Le clair est le défaut ; la palette sombre est conditionnée par un attribut `data-theme="dark"` qu'un petit store zustand (`ui/src/theme.ts`) pose sur `` et mémorise en localStorage, et Monaco lit ce store plutôt que la préférence de l'OS pour que l'éditeur ne contredise jamais la page. L'ancien comportement — suivre `prefers-color-scheme` — a été abandonné au profit d'un choix explicite, parce qu'un serveur Ori partagé est souvent consulté depuis des machines dont l'utilisateur ne maîtrise pas les réglages système (le navigateur d'une sandbox, le portable d'un collègue). Comme la largeur de l'arborescence, le thème est une commodité par navigateur : il vit hors du protocole WebSocket et le serveur n'en entend jamais parler. ## Pourquoi le serveur possède la conversation Le bridge enregistre chaque événement de la session et le rejoue à chaque connexion ; il renvoie même les prompts de l'utilisateur sous forme d'événements `user_message`. Le serveur devient ainsi l'unique source de vérité : la SPA ne fait jamais confiance à sa mémoire locale, elle se remet à zéro à chaque `hello` et se reconstruit depuis le rejeu. Le bénéfice : les reconnexions (mise en veille, coupure réseau, second onglet) sont trivialement correctes — tous les navigateurs convergent vers le même fil — au prix d'un historique borné en mémoire (4096 événements). ## Pourquoi les payloads ACP traversent le WebSocket tels quels Le bridge aurait pu traduire les updates ACP dans ses propres formes de messages. Il ne le fait délibérément pas : `session_update` et `permission_request` transportent les objets ACP bruts. Le protocole est bien documenté et il évolue (plans, modes, contenus d'outils plus riches) ; le relayer tel quel signifie que les nouveaux types d'update atteignent le front sans toucher au code Go, et que le front peut les adopter à son rythme — les types inconnus sont ignorés par construction. ## Pourquoi un processus adaptateur pour Claude Code La CLI Claude Code n'a pas de mode ACP natif (vérifié sur la v2.1.274 : aucune option ni sous-commande de ce type). Ori lance donc [`@agentclientprotocol/claude-agent-acp`](https://www.npmjs.com/package/@agentclientprotocol/claude-agent-acp), l'adaptateur du projet Agent Client Protocol pour le Claude Agent SDK, qui expose ACP sur stdio. L'adaptateur apporte sa propre CLI Claude Code via le paquet natif par plateforme du SDK (2.1.274 avec le SDK 0.3.274) : la version qui répond ne dépend pas de ce qui est installé sur la machine. Jusqu'au 18 septembre 2026, Ori utilisait [`@zed-industries/claude-code-acp`](https://www.npmjs.com/package/@zed-industries/claude-code-acp) de Zed. Cet adaptateur fige un SDK plus ancien dont la CLI embarquée (2.1.44) est désormais refusée par l'API pour les modèles actuels (« Claude Code 2.1.44 does not support this model; version 2.1.251 or newer is required »), ce qui cassait chaque prompt. Les deux adaptateurs parlent le même ACP : le changement est une simple valeur par défaut, `--agent-cmd` accepte toujours l'un ou l'autre. ## Alternatives écartées - **Piloter directement le mode `stream-json` de la CLI `claude`** — un pont maison vers le format de streaming propriétaire de Claude aurait demandé plus de travail, lié Ori à un seul agent, et renoncé à l'écosystème des agents ACP (Gemini CLI, et ce qui viendra ensuite). - **Server-sent events ou polling au lieu d'un WebSocket** — les demandes de permission exigent des messages dans les deux sens sur une même connexion ; le SSE aurait imposé un second canal pour les réponses. - **Plusieurs sessions simultanées dès la v1** — le bridge pilote délibérément une seule session d'agent partagée par tous les navigateurs connectés. L'interface `Prompter` et le modèle d'abonnement par connexion sont les coutures où le multi-sessions pourra s'ajouter sans refondre la conception. ## Comment les tests reflètent l'architecture Chaque couture a son double : le package agent est testé contre un agent ACP en mémoire (sans sous-processus), le bridge contre une session factice (sans agent), le reducer de la SPA contre de simples objets messages (sans socket), et un test de bout en bout compile le vrai binaire de l'agent mock et traverse le câblage de production complet via un vrai WebSocket. Voir [lancer les tests](../how-to/run-the-tests.md).