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.
7.6 KiB
#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.pyTests :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 :
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.obsigate:file-written— à chaque événement SSEtoolréussi d'un outil d'écriture (edit_file,append_to_file,create_file,restore_backup),bookslm.jsdiffuse un événement{vault, path}.- 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
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.