diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4ec2253..2595f69 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: Frontend unit tests run: node tests/frontend/unit.test.mjs - - name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop) + - name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition) run: | cd tests/frontend if [ -d node_modules ]; then @@ -52,6 +52,7 @@ jobs: node semantic-search.test.mjs node desktop.test.mjs node toolbar-order.test.mjs + node editor-inline.test.mjs else echo "tests/frontend/node_modules missing - installing jsdom" npm install --no-audit --no-fund --silent @@ -65,6 +66,7 @@ jobs: node semantic-search.test.mjs node desktop.test.mjs node toolbar-order.test.mjs + node editor-inline.test.mjs fi # ── Tests ───────────────────────────────────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cfbc1..66434ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,28 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Tests : `tests/frontend/ai.test.mjs` (82), `tests/test_bookslm.py::TestToolEventSources` (4), `tests/test_web_tools.py` (10). +### Modifié + +- **#93 Édition inline — « Editer » et « Forge » remplacent la vue lecture** : le conteneur + d'édition (`#editor-container`, CodeMirror ou iframe Forge) est déplacé **dans la zone de + contenu du document** (`#content-area`, ou le panneau actif en vue fractionnée) au lieu + d'être affiché dans l'overlay plein écran : le mode édition **remplace** le document en mode + lecture (bandeau, métadonnées, tags et contenu). L'overlay reste monté mais neutralisé + (transparent, sans capture des clics, `z-index` conservé) car le ruban d'édition mobile y est + ancré. Retour à la lecture par ✓ (sauvegarde), ✕, Échap ou l'ouverture d'un autre fichier — + un garde-fou dans `renderFile()` libère proprement la session (destruction CodeMirror/Yjs, + retrait de l'iframe Forge) ; l'overlay n'est conservé qu'en repli, quand la cible n'est pas le + document affiché (ex. fichier créé depuis la palette). Conséquence directe : l'éditeur ne + recouvre plus le panneau de l'assistant IA, qui peut donc mettre le document à jour **sous + les yeux de l'utilisateur** — `app_context.editing` annonce au modèle le document en cours + d'édition (et sa surface), et toute écriture d'outil (`edit_file`, `append_to_file`, + `create_file`, `restore_backup`) recharge le document affiché depuis le disque + (`obsigate:file-written`) : le tampon de l'éditeur est rafraîchi au lieu d'écraser la + modification de l'assistant par son auto-sauvegarde, et l'iframe Forge reçoit + `parent-reload`. Aucun identifiant DOM n'a changé (autosave, collab Yjs, barre IA, aperçu + Mermaid, Forge intacts). Fiche : [docs/features/editeur-inline.md](./docs/features/editeur-inline.md). + Tests : `tests/frontend/editor-inline.test.mjs` (19), `tests/test_bookslm.py::TestGeneralPrompt` (3 nouveaux). + ### Corrigé - **#91 — Bulle utilisateur trop étroite & ancre d'envoi retardée** : le plafond de diff --git a/backend/bookslm.py b/backend/bookslm.py index 8c5f153..832d09c 100644 --- a/backend/bookslm.py +++ b/backend/bookslm.py @@ -529,6 +529,17 @@ def _format_app_context(app_context: dict[str, Any] | None, recent_files: list[d if rendered_docs: lines.append("- Documents ouverts dans les onglets/panneaux : " + ", ".join(rendered_docs)) + editing = app_context.get("editing") + if isinstance(editing, dict) and editing.get("path"): + surface = "Forge" if editing.get("surface") == "forge" else "l'éditeur" + location = f"{editing['path']} (vault {editing['vault']})" if editing.get("vault") else str(editing["path"]) + lines.append( + f"- Document en cours d'édition dans {surface} : {location} — c'est le document affiché à la " + "place de la vue lecture. Pour le mettre à jour, utilise les outils d'écriture " + "(`edit_file`, `append_to_file`) : la modification est rechargée automatiquement dans " + "l'éditeur et la vue lecture dès l'exécution de l'outil." + ) + query = app_context.get("search_query") if query: total = app_context.get("search_total") diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 622db91..4c44ac0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.3.0-dev | **Dernière mise à jour :** 2026-09-14 +> **Version :** 2.3.0-dev | **Dernière mise à jour :** 2026-09-15 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** @@ -188,6 +188,7 @@ | 83 | Barre d'outils d'édition mobile — ruban style Obsidian Android | 2.3.0 | [archive](./archive/COMPLETED_v1-v2.md) | | 88 | Assistant IA — contexte applicatif (documents ouverts, répertoire, recherche, fichiers récents) & liens de fichiers fiables (BUG-041, BUG-042) | 2.3.0 | [features/ai-app-context.md](./features/ai-app-context.md) | | 91 | Assistant IA — Zone de discussion façon Notion : post ancré en haut, fournisseur/modèle discret & barre d'actions | 2.3.0 | [features/ai-assistant-conversation-ux.md](./features/ai-assistant-conversation-ux.md) | +| 93 | Édition inline — « Editer » et « Forge » remplacent la vue lecture (assistant IA qui met le document à jour) | 2.3.0 | [features/editeur-inline.md](./features/editeur-inline.md) | --- @@ -195,7 +196,7 @@ | Priorité | Items | Effort total estimé | |---|---|---| -| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #88–91 | ~107 jours réalisés | +| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #88–93 | ~108 jours réalisés | | 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour | | ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours | | ⚪ P2 restant | #92 Assistant IA — écosystème d'outils phase 2 (web étendu, sources connectées, documents) | 3-5 jours | diff --git a/docs/features/editeur-inline.md b/docs/features/editeur-inline.md new file mode 100644 index 0000000..9ac3815 --- /dev/null +++ b/docs/features/editeur-inline.md @@ -0,0 +1,125 @@ +# #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 | +| `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. diff --git a/frontend/editor-poc.html b/frontend/editor-poc.html index 18d7e53..54647c7 100644 --- a/frontend/editor-poc.html +++ b/frontend/editor-poc.html @@ -1450,6 +1450,16 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig window.addEventListener('message', function(e) { if (!e.data || !e.data.type) return; if (e.data.type === 'parent-save') { forceSave(); } + // #93 — the parent reloads the document after an external write (AI + // assistant edit_file / append_to_file / create_file): re-read from disk + // and drop the stale local buffer that would otherwise be autosaved back + // over the assistant's change. + if (e.data.type === 'parent-reload') { + clearTimeout(saveTimer); + isDirty = false; + loadFile(); + updateSaveDot(); + } if (e.data.type === 'parent-delete') { if (!fileVault || !filePath) return; if (!confirm('Supprimer definitivement ' + filePath + ' ?')) return; diff --git a/frontend/index.html b/frontend/index.html index 2636ad6..018b2a6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1211,7 +1211,13 @@ apercu rapide. Cliquez sur Forge dans la barre d'outils pour l'editeur - avance.Editer et + Forge remplacent + le document en mode lecture + (retour par ✓, ✕ ou + Echap) : le + panneau de l'assistant IA reste + accessible a cote. @@ -1370,9 +1376,11 @@ - +
Lecture
Lecture