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.
128 lines
7.6 KiB
Markdown
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.
|