feat(agent): vue chat frontend + routage cellKind (D5) — §17

Le frontend consomme l'exécution structurée livrée en D4 :
- AgentChatView : jumeau chat de TerminalView. Accumule les textDelta du
  tour courant, badges toolActivity, fige sur final (le contenu final
  remplace les deltas, pas de double rendu). Saisie Enter/Shift+Enter.
  Au montage : reattach (rejoue le scrollback) ou launch ; au démontage :
  détache seulement (jamais de close). Vue pure pilotée par le port.
- LayoutGrid/LeafView : routage cellKind — AgentChatView si "chat",
  TerminalView sinon (chemin PTY inchangé). Cache cellKindBySession pour
  router correctement après ré-attache/navigation.
- Port AgentGateway : sendPrompt / reattachChat / closeAgentSession ;
  cellKind sur TerminalHandle. Adapter Tauri (invoke + Channel<ReplyChunk>)
  et mock (MockChatSession, _setChatAgents) étendus.
- Types TS mirrors : CellKind, ReplyChunk, ReattachChatDto + cellKind sur
  TerminalSession.

Tests (QA) : 21 Vitest verts — routage chat/pty (non-régression terminal),
accumulation/non-doublon (garde anti-always-green delta!=final), ré-attache
sans re-spawn, envoi Enter vs Shift+Enter, badges toolActivity, mock stream.
npx vitest run : 345 passed, 0 failed. npm run build vert.

Reste D6 (messagerie inter-agents via send_blocking) et D7 (menu restreint
Claude/Codex + retrait custom) pour clore §17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:30:20 +02:00
parent f4d5727a69
commit 5059f37890
11 changed files with 1478 additions and 30 deletions

View File

@ -0,0 +1,99 @@
# Brief Dev — Lot D5 : frontend chat (§17.9)
> Demandé par **Main** à **DevFrontend** (dev) + **QA** (test). Cycle §3 : code → tests → vert.
> Périmètre **frontend uniquement** (`frontend/src`). Le backend D4 est livré et committé (`f4d5727`).
## 0. Où on en est
Le fil §17 (exécution structurée des agents IA) est livré jusqu'à **D4 inclus** côté backend :
les commandes Tauri `agent_send` / `reattach_agent_chat` / `close_agent_session` existent et
streament des `ReplyChunk` sur un `Channel`. D5 = **la vue chat React** qui consomme ça, plus
le **routage par type de cellule** dans le layout.
### Contrats backend exacts à mirrorer (déjà livrés)
Commandes Tauri (`crates/app-tauri/src/commands.rs`) :
- `agent_send(sessionId: string, prompt: string, onReply: Channel<ReplyChunk>) -> void`
- `reattach_agent_chat(sessionId: string, onReply: Channel<ReplyChunk>) -> ReattachChatDto`
- `close_agent_session(sessionId: string) -> void`
DTO (`crates/app-tauri/src/dto.rs`) — **sérialisation camelCase tagué `kind`** :
```ts
type ReplyChunk =
| { kind: "textDelta"; text: string }
| { kind: "toolActivity"; label: string }
| { kind: "final"; content: string };
// ReattachChatDto : le scrollback de conversation (chunks déjà streamés)
interface ReattachChatDto { sessionId: string; scrollback: ReplyChunk[]; }
// Et surtout : le DTO de session porte désormais cellKind
type CellKind = "pty" | "chat"; // toujours présent sur TerminalSessionDto
```
> `cellKind` vaut `"chat"` quand l'agent est piloté en mode structuré (profil Claude/Codex),
> `"pty"` pour un terminal brut. C'est **la seule info dont le frontend a besoin** pour router
> cellule chat vs terminal.
## 1. Périmètre D5 (réf. tableau §17.9 ligne D5)
1. **`AgentChatView`** (nouveau, `frontend/src/features/chat/`) : vue de conversation IdeA.
- Affiche les **deltas live** (accumulation `textDelta` → texte du tour en cours), l'**activité
d'outil** (`toolActivity`), et **fige le tour** sur `final`.
- Zone de **saisie** d'un prompt → appelle `AgentGateway.sendPrompt`.
- **Scrollback de conversation** : à l'attache, repeint l'historique renvoyé par
`reattachChat` ; survit à un changement d'onglet/layout (ré-attache, pas re-spawn).
- C'est le **jumeau chat** de `TerminalView` (`frontend/src/features/terminals/TerminalView.tsx`,
283 l.) — inspire-toi de sa gestion de cycle de vie (mount/attach/detach), mais pour un
flux de messages structuré au lieu d'octets xterm.
2. **Routage par `cellKind` dans `LayoutGrid`** (`frontend/src/features/layout/LayoutGrid.tsx`,
fonction `LeafView` ~l.159) : une cellule rend `AgentChatView` si `cellKind === "chat"`,
sinon `TerminalView` (comportement actuel inchangé). Le `cellKind` arrive sur le handle/session
au lancement (sortie de `launchAgent`) — propage-le jusqu'au leaf.
3. **Port `AgentGateway`** (`frontend/src/ports/index.ts`, interface ~l.76) : ajoute
- `sendPrompt(sessionId: string, prompt: string, onReply: (c: ReplyChunk) => void): Promise<void>`
- `reattachChat(sessionId: string, onReply: (c: ReplyChunk) => void): Promise<ReplyChunk[]>`
- `closeAgentSession(sessionId: string): Promise<void>`
(signatures à aligner avec le style des méthodes existantes `launchAgent`/`reattach`).
4. **Adapter Tauri** (`frontend/src/adapters/agent.ts`, classe `TauriAgentGateway`) : implémente
les 3 méthodes via `invoke(...)` + `new Channel<ReplyChunk>()` (modèle déjà présent pour
`launchAgent`/`reattach` qui utilisent `Channel<number[]>`).
5. **Mock gateway** (`frontend/src/adapters/mock/index.ts`) : streame des `ReplyChunk` scriptés
(quelques `textDelta` puis un `final`) pour les tests et le dev hors-Tauri.
6. **Types TS** : ajoute `ReplyChunk`, `CellKind`, `ReattachChatDto` aux types partagés (là où
vivent les autres mirrors de DTO), et le champ `cellKind` sur le type de session/handle.
## 2. Invariants à respecter
- **Zéro régression terminal** : `TerminalView` et le chemin PTY de `LayoutGrid` restent
identiques ; une cellule `pty` se comporte exactement comme avant.
- **Ré-attache ≠ re-spawn** : changer d'onglet puis revenir repeint la conversation depuis le
scrollback renvoyé par `reattachChat`, sans relancer de tour (miroir du PTY reattach).
- **Accumulation correcte** : les `textDelta` s'accumulent dans le tour courant ; `final` clôt
le tour (le texte final fait foi). Pas de doublon delta/final affiché deux fois.
- Frontières : la vue dépend du **port** `AgentGateway`, jamais directement de `invoke`/Tauri
(c'est l'adapter qui parle à Tauri). Hexagonal côté front respecté.
## 3. Tests attendus (QA — Vitest, réf. colonne « Tests attendus » D5 du §17.9)
Aligne-toi sur le style des `*.test.tsx` existants (`LayoutGrid.test.tsx`, `TerminalView.test.tsx`,
`adapters/agent.test.ts`), avec le **mock gateway** :
- cellule `cellKind:"chat"` rend `AgentChatView` ; `cellKind:"pty"` rend `TerminalView`.
- les `textDelta` s'accumulent à l'écran → `final` fige le tour.
- ré-attache repeint le scrollback **sans** re-spawn (le mock ne reçoit pas de nouveau `sendPrompt`).
- envoi d'un prompt → `AgentGateway.sendPrompt` appelé avec les bons args.
- `toolActivity` affiché comme activité d'outil.
- mock gateway streame bien une séquence `ReplyChunk` (deltas… puis final).
## 4. Méthode
Cycle §3 strict : DevFrontend code → QA écrit + exécute les tests Vitest → vert avant de clore.
Vérifie `npm run build` (tsc --noEmit + vite) **et** `npx vitest run` verts. **Ne pas committer,
ne pas push** — Main relit et commit. Rapport d'erreurs clair si rouge → correction → re-test.
## 5. Références
- Jumeau à copier : `frontend/src/features/terminals/TerminalView.tsx` (cycle de vie attach/detach).
- Routage cellule : `frontend/src/features/layout/LayoutGrid.tsx` (`LeafView`).
- Port : `frontend/src/ports/index.ts` (`AgentGateway`) ; adapter : `frontend/src/adapters/agent.ts` ;
mock : `frontend/src/adapters/mock/index.ts`.
- Backend déjà livré : commit `f4d5727`, `crates/app-tauri/src/{chat,commands,dto}.rs`.
- Spec : `ARCHITECTURE.md` §17.6 (deux types de cellules) et tableau §17.9 ligne **D5**.