# La vue embarquée

`show_schedule` affiche un planning **vivant** dans la conversation : pas une image ni un
export, mais le Gantt de PalmaPlan, peint par le même code que l'écran. Il est éditable si
votre rôle le permet et se tient à jour quand quelqu'un d'autre écrit.

```
show_schedule { schedule: "Migration S/4HANA", view: "gantt", critical_path: true }
```

Options :

- `view` : `gantt` (par défaut), `kanban` ou `agenda` ;
- `filter` : la syntaxe du volet Filtre de l'écran ;
- `zoom` ;
- une fenêtre de dates : `from` et `to` ;
- le panneau : `grid`, `timeline`, ou les deux ;
- `critical_path` et `reference` (la version de référence) ;
- `editable: false` pour une vue qui lit seulement.

Le rôle réel de l'utilisateur plafonne toujours : `editable` ne donne jamais plus qu'un rôle
n'accorde.

## Ce qu'est la vue : une MCP App native

PalmaPlan sert une [MCP App](https://modelcontextprotocol.io) : la ressource
`ui://palmaplan/planning`, de type `text/html;profile=mcp-app`. C'est **une page unique** :
le HTML, le JavaScript et le CSS sont en ligne, et rien ne se charge d'ailleurs. `show_schedule`,
`what_if` et `diff_versions` la désignent (`_meta.ui.resourceUri`). Un hôte qui rend les MCP
Apps l'affiche dans la conversation, dans un cadre isolé. Elle s'ouvre en ligne, puis en
plein écran sur demande, aux couleurs du thème de l'hôte (clair ou sombre), dans sa langue
et son fuseau.

La vue **ne parle qu'à l'hôte**. Sa CSP n'ouvre aucun domaine : ni `connectDomains`, ni
`frameDomains`, ni `resourceDomains`. Elle lit et écrit en appelant les tools du serveur
(`tools/call`), que l'hôte relaie **sous le jeton ou l'OAuth du connecteur**. La vue n'a
donc ni jeton à elle, ni cadre imbriqué, ni accès direct à PalmaPlan. Trois tools lui sont
réservés (`_meta.ui.visibility: ["app"]`) ; l'hôte les cache au modèle :

| Tool | Ce qu'il rend |
|---|---|
| `get_schedule_page` | le planning par pages, chaque réponse sous 120 000 caractères (le plafond d'un résultat d'outil chez Claude est d'environ 150 000) |
| `get_changes_since` | les entrées du journal après un rang, pour rester à jour |
| `get_schedule_absences` | l'agenda : les noms des membres et les dates de leurs absences, jamais le motif ni l'e-mail |

## Les trois vues, et l'avant/après

- **Le Gantt** : en ligne, la fenêtre utile du planning et ses lignes occupées ; en plein
  écran, le planning entier, groupes compris.
- **Le kanban** (`view: "kanban"`) : les actions des blocs, en trois colonnes (à faire, en
  cours, faites). Chaque carte nomme son bloc.
- **L'agenda** (`view: "agenda"`) : les absences des membres, jour par jour, sans motif.
- **L'avant/après** : `what_if` (une proposition) et `diff_versions` (une comparaison)
  s'affichent comme un Gantt où l'état proposé est peint par-dessus l'ombre de l'état
  courant. Viennent ensuite la liste des changements (le bloc, l'ancienne et la nouvelle
  date), la fin du projet et les nouveaux risques d'absence. Une proposition porte deux
  boutons, « Accepter » et « Refuser » ; une comparaison se lit seulement.

**« Accepter » applique exactement ce que l'avant/après montre, et rien d'autre.**

- Le bouton appelle `apply_patch` avec les commandes de la simulation et le rang auquel
  elle a été faite (`expected_rank`). L'API n'écrit que si le planning est toujours à ce
  rang, et elle en juge dans la transaction même qui écrit. Si quelqu'un a écrit entre-temps,
  rien n'est écrit et la vue dit : « La proposition est périmée — demandez-en une nouvelle. »
- Si la simulation porte des changements que la liste et le Gantt ne savent pas montrer
  (des réglages, un membre de ligne, des notes…), « Accepter » n'est pas proposé. Le texte
  de `what_if` et la vue nomment ces « changements non représentés ».
- La vue n'accepte qu'`apply_patch`, sur le planning affiché.

## Les gestes

Dans le Gantt, quand le rôle le permet :

- choisir un bloc ; Maj+→ le décale d'une semaine ouvrée, → et ← d'une unité ;
- glisser un bloc, ou tirer son bord pour le redimensionner ;
- changer le statut d'une action ;
- Annuler et Rétablir (Ctrl/Cmd+Z, Maj+Ctrl/Cmd+Z) ;
- supprimer, toujours après une confirmation dans la vue.

Chaque geste appelle **un tool existant** (`move_periods`, `resize_period`,
`set_action_status`, `undo`…), comme le ferait l'agent. Le journal, l'annulation et le temps
réel sont donc ceux de l'écran. Une écriture refusée (rôle, verrou, conflit) s'affiche dans
la vue. Un lecteur voit une vue qui lit seulement.

**Le direct** : la vue relit le journal par `get_changes_since`, d'abord toutes les deux
secondes, puis de moins en moins souvent quand rien ne bouge (jusqu'à quinze secondes). Elle
s'arrête quand le cadre est caché. Chaque appel de `show_schedule` monte un nouveau cadre ;
une vue plus ancienne du même planning se retire d'elle-même et dit qu'une vue plus récente
est ouverte plus bas.

## Parler au modèle

- **Le contexte du modèle** (`ui/update-model-context`) : la vue lui dit ce qui est
  sélectionné (l'identifiant et le libellé du bloc), la fenêtre visible et la vue ouverte.
  « Décale ce bloc d'une semaine » vise donc le bon bloc.
- **« Demander à Claude… »** (`ui/message`) écrit dans la conversation, à propos du bloc
  choisi.
- **« Aide »** s'ouvre par l'hôte (`ui/open-link`) : un cadre isolé n'ouvre rien lui-même.

## La négociation : qui voit quoi

Un hôte MCP Apps annonce l'extension `io.modelcontextprotocol/ui` avec le type
`text/html;profile=mcp-app`. Le serveur lit trois voies, et la première qui l'annonce
suffit :

1. `capabilities.extensions["io.modelcontextprotocol/ui"]` à `initialize` (la voie de la
   spec stable) ;
2. `capabilities.experimental["io.modelcontextprotocol/ui"]` (l'ancienne voie) ;
3. `_meta["io.modelcontextprotocol/clientCapabilities"]` d'une requête : la voie du
   protocole sans état. Le SDK du serveur (1.x) ne parle pas encore ce protocole, mais la
   voie est lue.

Ce qui en dépend :

| | Un hôte MCP Apps | Un client sans MCP Apps (Claude Code) |
|---|---|---|
| `tools/list` | les 101 tools ; l'hôte cache au modèle les trois réservés à la vue | les **98 tools du modèle** : les trois réservés à la vue ne sont pas listés (rien ne les y cacherait au modèle, et `get_schedule_page` rend jusqu'à 120 000 caractères) |
| `show_schedule` | la vue ; un texte qui la résume ; **aucun lien ni jeton** | le texte et **le lien `/embed`** d'une heure |
| `what_if`, `diff_versions` | l'avant/après, et leur texte | leur texte |

L'appel d'un tool réservé reste servi à tous. La visibilité n'est pas une frontière de
droits : les mêmes données se lisent par `palmaplan://schedule/{id}/json`, sous les mêmes
droits. Et un hôte qui afficherait la vue sans avoir annoncé l'extension ne la trouve pas
vide.

## Ce que montre Claude Code : du texte

Claude Code n'affiche pas les MCP Apps. `show_schedule` y rend un **texte qui se suffit** :
le nom, la période, le nombre de groupes, de lignes, de blocs et de jalons, les risques
(liens violés, blocs à risque d'absence) et le lien d'une heure :

```
Planning « Migration S/4HANA » — du 2026-11-02 au 2026-12-18 ; 2 groupes, 6 lignes, 6 blocs, 1 jalon.
Risques : 1 bloc à risque d absence.
Cet hôte n'affiche pas les vues MCP Apps : cette adresse ouvre le planning, valable une heure, en lecture seule : http://127.0.0.1:4785/embed/<id>?t=…
```

Son `structuredContent` est compact :
`{ schedule_id, name, view, options, rank, read_only, shown_at, url, expires_at }`. Il ne
porte jamais le modèle, parce que Claude Code le donne au modèle à la place du texte. Les
98 tools du modèle sont éprouvés de même par un test : chacun rend un texte utile (un résultat, ou un
refus qui dit quoi corriger) et un `structuredContent` compact.

## Le lien `/embed`, pour les hôtes sans MCP Apps

Le lien ouvre l'écran de PalmaPlan, réduit au planning, dans un navigateur ou dans une
iframe d'un autre site. Il porte un jeton signé d'**une heure**, plafonné au planning et au
rôle réel. Ce lien :

- **lit seulement**, sauf si l'on demande `editable: true` ;
- sous son jeton, la liste des membres n'a pas d'e-mail et l'agenda n'a pas de motif ;
- ne se renouvelle pas lui-même : il expire, et la vue le dit.
- tombe avant son heure si ce qui l'a demandé ne vaut plus : l'agent révoqué, ou la session
  fermée (mot de passe changé, « Déconnecter partout », compte repris par le propriétaire de
  son adresse).

Ce n'est pas un jeton nommé : il n'apparaît pas dans *Réglages › Connexions et agents › Applications connectées* (voir
[Les jetons et les portées](jetons-et-portees.md)).

## Les limites

- **La taille** : un planning se lit par pages de 120 000 caractères au plus. Un texte plus
  long qu'une page entière (un libellé démesuré) arrive coupé, et la vue le dit d'une
  ligne.
- **Le direct passe par le sondage**, pas par le canal WebSocket : quelques secondes de
  délai, pas l'instantané de l'écran.
- **Pas d'origine stable** (`ui.domain`) : rien n'est gardé d'une conversation à l'autre
  dans la vue.
- **Mobile** : la hauteur en ligne est limitée par l'hôte, et le plein écran convient mieux
  pour modifier. À 390 px, « Aide » et « Plein écran » se replient sur leur icône.
- **Les écritures depuis la vue** passent par les mêmes tools que l'agent : une écriture que
  l'agent ne peut pas faire, la vue ne peut pas la faire non plus.

## Ce qui est prouvé, ce qui ne l'est pas

**Prouvé, dans le dépôt** :

- par l'hôte de recette (`scripts/hote-mcp-apps.ts`), construit sur l'`AppBridge` du SDK
  MCP Apps et piloté par Chromium. Il reproduit ce que la documentation publique de Claude
  dit de ses hôtes : deux cadres sur deux origines distinctes, la CSP construite depuis
  `_meta.ui.csp` et posée en en-tête, `frameDomains` ignoré, la visibilité des tools
  respectée ;
- par la recette du sprint 24 (vingt points) : chaque message du protocole joué, aucune
  violation de CSP, aucune requête de la vue vers quelque origine que ce soit ; un geste
  dans la vue retrouvé dans l'écran web et inversement ; le contexte du modèle ; l'avant/après
  et « Accepter », y compris la proposition périmée ; la négociation (les 98 tools du modèle sans la capacité, les 101 avec) ; un
  tool réservé refusé au modèle ; un tool au-delà du rôle refusé à la vue ; Claude Code en
  texte seul.

**Pas prouvé** : **aucun hôte réel n'a encore affiché cette vue**. Ni Claude Desktop, ni
claude.ai, ni un autre hôte MCP Apps. L'hôte de recette suit la documentation, pas le code
de Claude : sa CSP exacte, sa façon d'annoncer la capacité MCP Apps à `initialize`, sa
WebView mobile peuvent différer. En particulier, si un hôte réel affiche la vue **sans**
annoncer l'extension, le modèle n'y verra que les 98 tools du modèle, et la vue continuera de
fonctionner, puisque les appels réservés restent servis. La marche à suivre de l'essai réel
est dans [Brancher Claude Desktop](claude-desktop.md), section « L’essai de la vue dans Claude Desktop ».
Tant qu'il n'a pas été joué, considérez la vue comme **prouvée au protocole**, pas comme
**prouvée dans Claude**.
