## Lot D — actions contrôlées depuis Work

### Objectif produit

Rendre le panneau `Work` opérable sans changer sa nature : il reste un tableau de bord live/conversations, mais l’utilisateur peut agir sur l’état déjà visible.

Périmètre minimal prioritaire :
- retrouver ou afficher la cellule d’un agent live ;
- rattacher une session déjà live à une cellule existante et déterminée ;
- arrêter explicitement un agent live ;
- voir davantage de la preview conversationnelle Lot C ;
- copier le résumé affiché.

Exclus :
- pas de lancement d’un agent neuf depuis Work ;
- pas de création automatique de cellule/layout ;
- pas de cancel/requeue/priorisation FIFO ;
- pas de modification de handoff, mémoire automatique ou Lot E ;
- pas de lecture/exposition brute de `log.jsonl` ;
- pas de transcript complet.

### Actions à livrer

Priorité 1 — `Open` / `Go to cell`
- Si `agent.live.nodeId` correspond à une cellule visible dans le layout actif : focus/scroll/highlight de cette cellule.
- Aucun appel backend. Aucun spawn.
- Libellé UI : `Open` ou `Go to cell` selon la place.

Priorité 2 — `Attach`
- Si l’agent est live mais sa cellule hôte n’est plus visible, rattacher la session à une cellule existante et non ambiguë.
- Cible autorisée pour ce lot : une cellule visible déjà pinnée sur le même agent et sans session, ou une cellule explicitement sélectionnée par l’utilisateur dans le layout.
- Si aucune cible déterministe : action désactivée avec message court du type `Pin this agent to a cell first`.
- Ne jamais créer une nouvelle session. Ne jamais appeler `launch_agent`.

Priorité 3 — `Stop`
- Arrête la session live de l’agent après confirmation.
- Fonctionne pour PTY et structured.
- Ne supprime pas l’agent, ne supprime pas le handoff, ne vide pas `conversationId`.
- Après succès : refresh Work + layout/live state via events existants ou refresh explicite.

Priorité 4 — `View conversation`
- Expansion inline dans Work sur la base de `ProjectWorkState.conversations[]`.
- Montre objectif, summary preview, et les `recentTurns` déjà bornés par Lot C.
- Pas de nouveau endpoint de transcript.

Priorité 5 — `Copy summary`
- Frontend-only via clipboard.
- Copie uniquement le texte déjà exposé par Lot C : objectif + summary preview + recent turns bornés si summary absent.
- Désactivé si aucune preview exploitable.

### Backend

Réutiliser :
- `GetProjectWorkState` : source unique du panneau, inchangé fonctionnellement ;
- `LiveSessions` / `TerminalSessions` / `StructuredSessions` : source de vérité live ;
- `LayoutGateway.mutateLayout` avec `setSession` / `setCellAgent` ;
- `close_terminal` et `close_agent_session` comme primitives existantes ;
- `attach_live_agent` existant, mais à durcir.

Contrats backend recommandés :

1. Faire évoluer `attach_live_agent` en vrai use case applicatif `AttachLiveAgent`.

Entrée :
```rust
pub struct AttachLiveAgentInput {
    pub project: Project,
    pub agent_id: AgentId,
    pub node_id: NodeId,
}
```

Sortie :
```rust
pub struct AttachLiveAgentOutput {
    pub agent_id: AgentId,
    pub node_id: NodeId,
    pub session_id: SessionId,
    pub kind: LiveSessionKind,
}
```

Comportement :
- chercher la session live dans `TerminalSessions`, sinon `StructuredSessions` ;
- rebind `node_id` sans spawn ;
- retourner `NOT_FOUND` si l’agent n’est plus live ;
- idempotent si déjà attaché à cette cellule ;
- ne touche pas `ConversationLog`, `HandoffStore`, `ProviderSessionStore`.

2. Ajouter `StopLiveAgent` plutôt que faire deviner le registre au frontend.

Entrée :
```rust
pub struct StopLiveAgentInput {
    pub project: Project,
    pub agent_id: AgentId,
}
```

Sortie :
```rust
pub struct StopLiveAgentOutput {
    pub agent_id: AgentId,
    pub session_id: SessionId,
    pub kind: LiveSessionKind,
}
```

Comportement :
- résoudre la session live par agent ;
- si PTY : déléguer à `CloseTerminal` / `PtyPort.kill` ;
- si structured : `StructuredSessions::remove` puis `AgentSession::shutdown()` ;
- publier `DomainEvent::AgentExited { agent_id, code }` ou l’équivalent déjà relayé ;
- no-op contrôlé ou `NOT_FOUND` si déjà arrêté. Préférence : `NOT_FOUND` côté API, frontend l’interprète comme refresh.

Pourquoi pas seulement `close_terminal(sessionId)` : `WorkState.live.kind` existe, et le modèle a deux registries. Une commande par agent garde la cohérence single-agent côté backend.

### Tauri / DTO

Nouvelles/évolutions :

```ts
attach_live_agent({ request: { projectId, agentId, nodeId } })
  -> { agentId, nodeId, sessionId, kind }

stop_live_agent({ request: { projectId, agentId } })
  -> { agentId, sessionId, kind }
```

`LiveAgentDto` peut être réutilisé si on lui ajoute/garantit `kind`; sinon créer un DTO dédié. Préférence : enrichir `LiveAgent` additivement avec `kind: "pty" | "structured"`, cohérent avec `LiveWorkSession.kind`.

Pas de commande `get_conversation_log`, `open_conversation`, `copy_summary`.

### Frontend

Ports UI :
- `AgentGateway.attachLiveAgent(projectId, agentId, nodeId): Promise<LiveAgent>` doit retourner `kind` ;
- ajouter `AgentGateway.stopLiveAgent(projectId, agentId): Promise<StoppedLiveAgent>` ;
- ne pas faire appeler `close_terminal` directement par `ProjectWorkStatePanel` pour stopper un agent ;
- `LayoutGateway.loadLayout/mutateLayout` réutilisés pour trouver/attacher la cellule cible.

Coordination UI recommandée :
- extraire le helper de focus cellule (`goToCell`) de `LayoutGrid` vers `features/layout/focusCell.ts` ;
- ajouter un petit hook `useWorkActions(projectId, activeLayoutId?)` consommé par `ProjectWorkStatePanel` ;
- le hook sait charger le layout actif, trouver les feuilles, vérifier si `live.nodeId` est visible, et appeler `attachLiveAgent + mutateLayout(setCellAgent/setSession)` si une cible déterministe existe.

### Sécurité / cohérence

Règles obligatoires :
- `Open` et `Attach` ne spawnent jamais. Interdit d’appeler `launchAgent` depuis Work dans Lot D.
- `Attach` ne doit pas écraser une cellule qui héberge déjà une session différente, sauf remplacement explicite futur hors lot.
- `Stop` demande confirmation ; c’est une action destructive sur le process, mais pas sur les fichiers.
- `Stop` ne supprime ni agent, ni tickets, ni conversation summary, ni handoff.
- `Stop` doit libérer la live registry pour préserver l’invariant “un agent = une session live max”.
- Si un ticket est `inProgress`, afficher dans la confirmation que le tour en cours sera interrompu ; ne pas tenter de résoudre le ticket dans ce lot.
- Pas de permission filesystem nouvelle. Les seules mutations durables sont les mutations layout déjà existantes (`setSession`/`setCellAgent`) lors d’un attach.

### UI minimale

Dans chaque ligne agent :
- bouton `Open` si live ;
- bouton `Attach` si live mais cellule non visible et une cible existe ;
- bouton `Stop` si live ;
- état désactivé explicite si action impossible.

Dans chaque ticket/summary :
- bouton disclosure `View` pour ouvrir/fermer les détails Lot C ;
- bouton `Copy` à côté du summary, pas sur toute la ligne ;
- garder l’UI dense : pas de drawer global, pas de modal conversation, pas de rendu Markdown lourd.

Feedback :
- erreurs action en inline dans le panneau ;
- après `Stop`/`Attach`, refresh `getProjectWorkState` et layout actif ;
- `Copy` affiche un état bref `Copied`.

### Tests attendus

Application Rust :
- `AttachLiveAgent` rebind PTY sans changer `session_id` ;
- `AttachLiveAgent` rebind structured sans changer `session_id` ;
- attach agent absent => `NOT_FOUND` ;
- attach même node => idempotent ;
- `StopLiveAgent` arrête PTY et retire la session live ;
- `StopLiveAgent` arrête structured via `shutdown()` et retire la session live ;
- stop absent => `NOT_FOUND` ;
- stop ne touche pas handoff/log/provider sessions.

Tauri DTO/tests :
- request camelCase pour `attach_live_agent` et `stop_live_agent` ;
- response sérialise `kind`;
- erreurs `INVALID` ids et `NOT_FOUND` live session.

Frontend tests :
- `Open` focus une cellule visible et n’appelle aucun launch ;
- `Attach` appelle `attachLiveAgent` puis `mutateLayout(setSession)` sur une cible déterministe ;
- `Attach` désactivé sans cible ;
- `Stop` demande confirmation puis appelle `stopLiveAgent`, refresh Work ;
- `Stop` sur agent busy affiche l’avertissement ;
- `View conversation` n’affiche que les champs Lot C ;
- `Copy summary` copie uniquement la preview exposée, pas de log brut ;
- pas de régression des refresh events Lot A/B/C.

Commandes :

```bash
cargo test -p application terminal_usecases structured_registry workstate
cargo test -p app-tauri dto_agents list_live_agents_r0b
npm --prefix frontend test -- workstate
npm --prefix frontend test -- LayoutGrid singletonAgent agentAlreadyRunning
```

Puis en validation large :

```bash
cargo test
npm --prefix frontend test
```

### Décision de cadrage

Lot D doit être “actions sur état existant”, pas “nouveau centre de contrôle”. Le seul ajout backend vraiment justifié est de faire porter `attach/stop` par des contrats agent-level qui respectent PTY + structured et l’invariant single-agent. La conversation reste une preview bornée issue de Lot C ; le log brut et la mémoire automatique restent hors périmètre.