Files
ObsiGate/docs/features/editeur-inline.md
T
bruno 788d84a2bf
CI / lint (push) Successful in 1m27s
CI / security (push) Successful in 57s
CI / test (push) Successful in 2m52s
CI / build (push) Successful in 53s
CI / e2e (push) Successful in 10m45s
fix(editeur): #93 garde-fous de session d'edition inline (onglets, ecriture externe) + hooks de versionnage documentes
Une session d'edition inline (Editer/Forge) qui possede la zone de contenu etait detruite par deux chemins qui vident cette zone : activation d'onglet (TabManager.activate, PaneTabManager.activate) -> detachInlineEditor() avant le placeholder ; evenement SSE index_updated -> reloadExternalWrite() recharge le tampon de l'editeur au lieu de re-rendre la vue lecture. Nouveau helper queryEditor() ; closeEditor() remet le conteneur dans la modale avant de restaurer en-tete/pied/marque. docs/CONTRIBUTING.md documente scripts/install-hooks.sh.
2026-09-16 11:43:41 -04:00

128 lines
7.6 KiB
Markdown

# #93 — Édition inline : « Editer » et « Forge » remplacent la vue lecture
> **Statut :** 🟢 livré (en attente de vérification utilisateur)
> **Version :** 2.3.0
> **Composants :** `frontend/js/editor-inline.js` (nouveau), `frontend/js/utils.js`,
> `frontend/js/viewer.js`, `frontend/js/sync.js`, `frontend/js/bookslm.js`,
> `frontend/editor-poc.html`, `frontend/index.html`, `frontend/style.css`,
> `backend/bookslm.py`
> **Tests :** `tests/frontend/editor-inline.test.mjs` (19),
> `tests/test_bookslm.py::TestGeneralPrompt` (7)
## Contexte
Le mode édition (bouton **Editer**, CodeMirror) et **Forge** (éditeur avancé dans une iframe)
s'ouvraient dans `#editor-modal` : un overlay `position: fixed` plein écran. Le document restait
rendu **dessous**, mais invisible, et l'overlay recouvrait le panneau de l'assistant IA
(`.bookslm-panel`, `z-index: 100` contre `1000` pour la modale). Conséquences :
- le document n'était plus consultable pendant l'édition (pas de relecture côte à côte) ;
- l'assistant IA était inutilisable pendant l'édition : son action « Ajouter » (qui insère dans
`state.editorView`) exigeait une session d'édition ouverte… que l'utilisateur ne pouvait pas
regarder en même temps que l'assistant.
## Conception
### 1. Bascule du conteneur d'édition dans la zone de lecture
`frontend/js/editor-inline.js` (module autonome, sans dépendance) déplace le conteneur
`#editor-container` — qui contient le CodeMirror **ou** l'iframe Forge — de l'overlay vers la
zone de contenu :
```
#content-area (ou .pane-content de la pane active) ← le document en lecture
└── .editor-container.editor-inline ← l'éditeur, à la place du document
```
| Fonction | Rôle |
|---|---|
| `getEditorContainer()` / `getEditorModal()` | résolution par id (`#editor-container`, `#editor-modal`) |
| `getInlineHostArea()` | zone de lecture visée : override de rendu, pane active en vue fractionnée, sinon `#content-area` |
| `isInlineEditorActive()` | une session occupe-t-elle une zone de contenu ? |
| `mountEditorInline(area)` | `area.replaceChildren(container)` + classes `editor-inline` / `editor-inline-host` / `editor-inline-mode` |
| `unmountEditorInline()` | conteneur rendu à la modale, zone vidée, renvoie la zone à re-rendre |
Aucun identifiant DOM n'est modifié (`#editor-body`, `#editor-save`, `#editor-title-input`…) :
CodeMirror, l'auto-sauvegarde (2 s), la collaboration Yjs, la barre IA, l'aperçu Mermaid et
l'iframe Forge continuent de fonctionner sans réécriture. Seul l'**hôte** change.
L'overlay reste monté et garde la classe `active` (signal d'état utilisé par l'auto-save, le
raccourci Ctrl+J, la hauteur d'éditeur mobile…), mais passe en classe
`editor-inline-mode` : transparent, sans fond ni padding, `pointer-events: none` (le ruban
d'édition mobile `.me-ribbon`, ancré à la modale, conserve `pointer-events: auto`).
### 2. Cycle de vie
| Action | Effet |
|---|---|
| **Editer** / **Forge** | `activateInlineEditor(vault, path)` → bascule inline **si** la cible est le document affiché dans la zone de contenu |
| ✓ (Sauvegarder) | sauvegarde puis `closeEditor()` → retour à la lecture (contenu relu depuis le disque) |
| ✕ (Annuler) / **Échap** | `closeEditor()` → retour à la lecture (les modifications non sauvegardées sont abandonnées, comme avant) |
| Ouverture d'un autre fichier (`renderFile()`) | garde-fou : `detachInlineEditor()` libère la session (destroy CodeMirror/Yjs, retrait de l'iframe Forge) avant le rendu de la lecture |
| Activation d'un onglet (`TabManager.activate`, `PaneTabManager.activate`) | même garde-fou **avant** le placeholder de chargement : ces fonctions vident la zone de contenu, ce qui détruirait le DOM de l'éditeur |
| Événement SSE `index_updated` sur le fichier affiché | `reloadExternalWrite()` recharge le **tampon de l'éditeur** depuis le disque au lieu de re-rendre la vue lecture (sans quoi la modification externe détruisait la session) |
| `forge-close` (message de l'iframe) | route vers `closeEditor()` — plus de manipulation manuelle de la modale dans `sync.js` |
**Repli overlay** : si la cible n'est pas le document affiché (fichier venant d'être créé via la
palette, par exemple), la modale plein écran est conservée. Le mode inline exige que la zone de
lecture soit celle du fichier édité — c'est ce qui garantit que « remplacer » signifie bien
remplacer *ce* document.
L'invalidation du cache d'onglet (`_invalidateActiveTabCache`) évite qu'un aller-retour
d'onglets réaffiche le HTML **d'avant** l'édition.
### 3. Assistant IA — document mis à jour sous les yeux de l'utilisateur
L'éditeur n'occupe plus le panneau de l'assistant : les deux surfaces cohabitent. Trois
ajouts rendent la mise à jour effective de bout en bout :
1. **`app_context.editing`** — `bookslm.js::_editingDocument()` envoie
`{vault, path, surface: "editor"|"forge"}` quand une session d'édition est ouverte ;
`backend/bookslm.py::_format_app_context()` l'annonce dans le prompt Général
(« Document en cours d'édition dans … ») et indique au modèle d'utiliser les outils
d'écriture pour le mettre à jour.
2. **`obsigate:file-written`** — à chaque événement SSE `tool` réussi d'un outil d'écriture
(`edit_file`, `append_to_file`, `create_file`, `restore_backup`), `bookslm.js` diffuse un
événement `{vault, path}`.
3. **Rechargement** — `utils.js::reloadExternalWrite()` :
- fichier ouvert dans l'éditeur inline → le tampon CodeMirror est **remplacé** par le contenu
du disque (l'auto-sauvegarde de ce remplacement est neutralisée, sinon elle réécrivait
l'ancien texte par-dessus la modification de l'IA) ;
- fichier ouvert dans Forge → message `parent-reload` à l'iframe (`editor-poc.html`), qui
relit le fichier et abandonne son tampon périmé ;
- fichier simplement affiché en lecture → re-rendu depuis le disque.
Sans ce point 3, une édition par l'IA aurait été silencieusement écrasée par l'auto-sauvegarde de
l'éditeur deux secondes plus tard.
### 4. Feuille de style
`frontend/style.css` : `.content-area.editor-inline-host` devient une colonne flex sans padding,
et `.editor-container.editor-inline` la remplit (`flex: 1`, `height: auto` — pour battre le
`height: 100vh` de la media query mobile par spécificité, `max-width/max-height: none`, sans
bordure ni ombre). L'en-tête d'édition (titre, nom de fichier, pastille d'état, ✓/✕/🗑) sert
donc de barre d'outils du document, et le corps occupe la zone de lecture.
## Limites connues
- Les outils de **renommage / déplacement** de l'IA (`rename_file`, `move_path`) ne déclenchent
pas de rechargement : après un renommage, le document redevient accessible via l'arborescence.
- La collaboration Yjs reste liée à l'ouverture de l'éditeur : un changement d'onglet pendant
une session collaborative la termine (comportement identique à celui de la fermeture).
- Le mode inline suppose un document affiché correspondant à la cible ; les flux « création de
fichier » passent donc toujours par la modale.
## Vérification
```bash
node tests/frontend/editor-inline.test.mjs # 19 tests (DOM + câblage)
node tests/frontend/validate-imports.mjs
node tests/frontend/unit.test.mjs
.venv/Scripts/python.exe -m pytest tests/test_bookslm.py -k GeneralPrompt
```
Contrôle manuel sur l'instance de test (`http://localhost:2020`, `admin` / `test123`) : ouvrir un
document markdown → **Editer** (le document est remplacé par l'éditeur, bandeau/onglets toujours
visibles, panneau assistant cliquable) → **Forge** depuis la lecture (iframe plein cadre) →
✓ / ✕ / Échap → retour en lecture avec le contenu à jour.