feat(ai): ajouter une section de la reponse et supporter l'editeur Forge (BUG-057, #102)
CI / lint (push) Successful in 1m34s
CI / security (push) Successful in 1m1s
CI / test (push) Successful in 2m44s
CI / build (push) Successful in 1m1s
CI / e2e (push) Successful in 10m51s

L'assistant IA peut desormais inserer sa reponse dans l'editeur Forge (postMessage parent-insert) en plus d'Editer, et chaque bloc de code propose un bouton « Ajouter la section » pour n'inserer que ce bloc.
This commit is contained in:
2026-09-17 09:39:35 -04:00
parent 94ea5909f4
commit c0eea526de
19 changed files with 239 additions and 29 deletions
+24 -1
View File
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
> **En cours de développement** : les changements à venir sont listés dans la section
> [Unreleased](#unreleased). La dernière version livrée est **2.8.4**.
> [Unreleased](#unreleased). La dernière version livrée est **2.9.0**.
---
@@ -14,6 +14,29 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.9.0] — 2026-09-17
### Ajouté
- **#102 — Assistant IA : bouton « Ajouter la section » par bloc de code** : chaque bloc
de code d'une réponse de l'assistant (par ex. une section ```markdown```) affiche un
bouton discret « Ajouter la section » qui insère **uniquement ce bloc** dans le document
ouvert (sans les délimiteurs de code), au lieu de la réponse complète. Fichiers :
`frontend/js/bookslm.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`.
Tests : `tests/frontend/ai.test.mjs`.
### Corrigé
- **BUG-057 — Assistant IA : bouton « Ajouter » inopérant dans l'éditeur Forge** : le
bouton ne ciblait que `state.editorView` (CodeMirror de « Editer ») et affichait « Aucun
document ouvert dans l'éditeur » en Forge. `_insertIntoEditor()` prend désormais en
charge les trois surfaces : CodeMirror, l'iframe Forge (délégation par
`postMessage({ type: 'parent-insert' })` → `insertAtCursor` dans `editor-poc.html`) et le
textarea de repli. Fichiers : `frontend/js/bookslm.js`, `frontend/editor-poc.html`.
Tests : `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`.
---
## [2.8.4] — 2026-09-17
---
+3 -3
View File
@@ -4,7 +4,7 @@
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
[![Version](https://img.shields.io/badge/Version-2.8.4-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.9.0-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -916,8 +916,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.8.4).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.9.0).
---
*Projet : ObsiGate | Version : 2.8.4 | Dernière mise à jour : Juin 2026*
*Projet : ObsiGate | Version : 2.9.0 | Dernière mise à jour : Juin 2026*
+3 -3
View File
@@ -2,7 +2,7 @@
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
[![Version](https://img.shields.io/badge/Version-2.8.4-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.9.0-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -1085,8 +1085,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.8.4).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.9.0).
---
*Project: ObsiGate | Version: 2.8.4 | Last updated: May 2026*
*Project: ObsiGate | Version: 2.9.0 | Last updated: May 2026*
+1 -1
View File
@@ -1 +1 @@
2.8.4
2.9.0
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.8.4"
version = "2.9.0"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.8.4"
version = "2.9.0"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.8.4",
"version": "2.9.0",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+2
View File
@@ -166,6 +166,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-054* | [🟡 IMPORTANT] Éditeur « Editer » : le bouton Sauvegarder reste bloqué sur le spinner de chargement (retour au crochet uniquement après un refresh complet) | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/utils.js` | Ouvrir un fichier → Editer → cliquer Sauvegarder (ou Ctrl+S) ; rouvrir l'éditeur : le bouton reste un spinner désactivé | Nouveau helper `resetSaveButton()` (crochet `✓` + `disabled=false` + styles en ligne nettoyés) appelé à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas d'échec (`saveFile`). Tests : `tests/frontend/editor-inline.test.mjs` (+4) | Le nœud `#editor-save` est partagé entre sessions : l'état « spinner + désactivé » posé par une sauvegarde manuelle n'était jamais remis à zéro (succès → fermeture puis réouverture, Forge, ou échec réseau dans le `catch`). Seul un rechargement de `index.html` restaurait le crochet |
| *BUG-055* | [🟡 IMPORTANT] Éditeur Forge : l'autocomplétion (Tab) ajoute des espaces parasites, l'effacement détruit le mot complété et la complétion fantôme est illisible | 🟢 corrigé | P1 | 📱 frontend + ⚙️ backend | IA | `frontend/editor-poc.html`, `frontend/js/autocomplete.js`, `backend/ai.py`, `.gitea/workflows/ci.yml`, `tests/frontend/forge-completion.test.mjs` (nouveau) | Forge : taper un mot, puis Tab pour compléter ; un espace (voire deux) s'insère avant le mot complété, et le retour arrière efface l'ajout. La prédiction IA s'affichait décalée (texte miroir du document entier) | **Cause** : trois gestionnaires `keydown` Tab indépendants s'exécutaient tous — l'indentation (`insertAtCursor(' ')`) s'ajoutait à la complétion de mot et à l'acceptation du ghost. **Correctif** : gestion **unifiée** de Tab (`liste ouverte > ghost > mot du document > indentation`, une seule action), helpers purs partagés (`getWordFragment`, `findWordCompletions`, `normalizeGhost`, `chooseTabAction`) dans `autocomplete.js`, liste déroulante si plusieurs candidats, dropdown positionné au curseur, ghost **positionné au curseur** (fini le miroir du document, nettoyé au déplacement/scroll), complétion de mot sans espace garanti (`normalizeGhost` tronque au premier espace) et prompt `/api/ai/inline-complete` simplifié. Tests : `tests/frontend/forge-completion.test.mjs` (28). | Cause du bug : l'indentation Tab n'était pas conditionnée à l'absence de suggestion. Le ghost re-rendait tout le texte transparent + prédiction, d'où l'impression d'espaces et les erreurs d'effacement |
| *BUG-056* | [🟡 IMPORTANT] Éditeur Forge en plein écran : l'Assistant IA s'ouvre en arrière-plan et reste invisible | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/editor-poc.html`, `frontend/js/sync.js`, `tests/frontend/forge-completion.test.mjs`, `tests/frontend/editor-inline.test.mjs` | Forge : passer en plein écran puis cliquer le bouton « Assistant IA » (ou `Ctrl+J`) — le panneau s'ouvre dans le document parent, masqué par l'iframe plein écran | Sortie du plein écran **avant** d'ouvrir le panneau, des deux côtés : côté iframe (`openAssistant` → `document.exitFullscreen()` puis `postMessage` à la résolution) **et** côté parent (`sync.js` sur `forge-open-ai` → `document.exitFullscreen()` puis `openForCurrentContext()`), car le plein écran peut être détenu par le document parent et non par l'iframe (dans ce cas `document.fullscreenElement` est nul dans l'iframe et sa sortie échoue). Tests : `forge-completion.test.mjs` (+1), `editor-inline.test.mjs` (+1) | Le panneau assistant est monté dans `document.body` du parent : l'API Fullscreen ne rend que l'élément plein écran et ses descendants, donc il ne peut pas s'afficher au-dessus de l'iframe Forge en plein écran. La sortie côté iframe seule ne suffisait pas quand le parent détient le plein écran |
| *BUG-057* | [🟡 IMPORTANT] Assistant IA : le bouton « Ajouter » est inopérant dans l'éditeur Forge (fonctionne seulement dans « Editer ») | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js`, `frontend/editor-poc.html` | Ouvrir un document dans Forge, demander une réponse à l'assistant puis cliquer « Ajouter » | `_insertIntoEditor()` cible Forge (`#forge-iframe`) : `postMessage({ type: 'parent-insert', text })` ; `editor-poc.html` insère au curseur (`insertAtCursor`) et marque le tampon modifié. Repli textarea inclus. Tests : `tests/frontend/ai.test.mjs` (+3), `tests/frontend/editor-inline.test.mjs` (+1) | `state.editorView` (CodeMirror) est nul en Forge : le clic affichait « Aucun document ouvert dans l'éditeur » |
| | | | | | | | | | | |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -225,6 +226,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-17 | BUG-055 | Correction | `frontend/editor-poc.html`, `frontend/js/autocomplete.js`, `backend/ai.py`, `.gitea/workflows/ci.yml`, `tests/frontend/forge-completion.test.mjs` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-055** : trois gestionnaires `keydown` Tab indépendants s'exécutaient à chaque appui — l'indentation (`insertAtCursor(' ')`) s'ajoutait à la complétion de mot (`insertAtCursor(suffixe)`) et à l'acceptation du ghost text, d'où l'espace parasite avant le mot complété puis un effacement destructeur. Gestion **unifiée** de Tab (`liste ouverte > ghost > mot du document > indentation`), helpers purs partagés (`getWordFragment`/`findWordCompletions`/`normalizeGhost`/`chooseTabAction`) extraits dans `autocomplete.js`, liste déroulante au curseur quand plusieurs mots correspondent, ghost **positionné au curseur** (plus de miroir du document entier, nettoyé au déplacement/scroll), complétion de mot sans espace garantie et prompt `/api/ai/inline-complete` simplifié (128 tokens). Vérifié : `tests/frontend/forge-completion.test.mjs` 28/28 (nouveau), `unit.test.mjs` 9/9, `editor-inline.test.mjs` 40/40, `ai.test.mjs` 88/88, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-17 | BUG-055 (complément) | Correction | `frontend/editor-poc.html`, `frontend/js/utils.js`, `tests/frontend/forge-completion.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-055 (complément)** : une complétion acceptée au `Tab` disparaissait 1–2 s plus tard. Cause : l'auto-sauvegarde (2 s) déclenche un `index_updated` SSE sur le fichier affiché, et `reloadExternalWrite` rechargeait le tampon Forge **depuis le disque**, écrasant toute frappe postérieure à la sauvegarde. Le rechargement SSE est désormais ignoré si le tampon est modifié (`parent-reload` sans `force` + `isDirty` ; garde équivalente sur le point d'auto-sauvegarde CodeMirror) ; seul `obsigate:file-written` (assistant IA) passe `force=true`. L'auto-sauvegarde ne remet plus l'état « enregistré » si des modifications sont arrivées pendant la requête (Forge + CodeMirror), et `acceptGhost()` annule la requête de prédiction en attente. Vérifié : `forge-completion.test.mjs` 31/31 (+3), `editor-inline.test.mjs` 41/41 (+1), 14 suites frontend vertes, validate-imports 38 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-17 | BUG-056 | Correction | `frontend/editor-poc.html`, `frontend/js/sync.js`, `tests/frontend/forge-completion.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-056** : en plein écran Forge, l'Assistant IA s'ouvrait en arrière-plan. La sortie du plein écran est désormais faite **côté iframe** (`openAssistant` → `document.exitFullscreen()` puis `postMessage` à la résolution) **et côté parent** (`sync.js` sur `forge-open-ai` → `document.exitFullscreen()` puis `openForCurrentContext()`), car le plein écran peut appartenir au document parent (l'iframe voit alors `fullscreenElement` nul et sa sortie échoue — c'était le cas non couvert par le premier correctif). Vérifié : `forge-completion.test.mjs` 32/32 (+1), `editor-inline.test.mjs` 42/42 (+1), 14 suites frontend vertes, validate-imports 38 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-17 | BUG-057, #102 | Correction + feature | `frontend/js/bookslm.js`, `frontend/editor-poc.html`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `docs/archive/COMPLETED_v1-v2.md`, `docs/ROADMAP.md`, `CHANGELOG.md` | **BUG-057** : le bouton « Ajouter » de l'assistant ne ciblait que `state.editorView` (CodeMirror) ; en Forge il affichait « Aucun document ouvert dans l'éditeur ». `_insertIntoEditor()` gère désormais les trois surfaces : CodeMirror, l'iframe Forge (`postMessage({ type: 'parent-insert', text })` → `insertAtCursor` dans `editor-poc.html`) et le textarea de repli. **#102** : chaque bloc de code d'une réponse reçoit un bouton « Ajouter la section » (`.bookslm-code-insert`, révélé au survol) qui insère le contenu du bloc sans les délimiteurs ` ``` `. Vérifié : `ai.test.mjs` 91/91 (+3), `editor-inline.test.mjs` 43/43 (+1), `forge-completion.test.mjs` 32/32, unit 9/9, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0. | 🟢 corrigé (en attente vérif utilisateur) |
---
+3 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.8.4 | **Dernière mise à jour :** 2026-09-17
> **Version :** 2.9.0 | **Dernière mise à jour :** 2026-09-17
> **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)**
@@ -199,6 +199,8 @@
| 99 | Sidebar — Filtrage des vues « Récents » et « Sauvegardes » | 2.6.0 | [features/sidebar-filters.md](./features/sidebar-filters.md) |
| 100 | Assistant IA — Deep Research en pastille (au lieu du texte injecté) | 2.6.0 | [features/ai-assistant-history.md](./features/ai-assistant-history.md) |
| 101 | Forge — Assistant IA partagé (bouton AI Panel = assistant, fournisseur/modèle configuré, autocomplétion) + plein écran Forge/Editer | 2.8.0 | [features/forge-assistant.md](./features/forge-assistant.md) |
| BUG-057 | Assistant IA — bouton « Ajouter » fonctionnel dans l'éditeur Forge (en plus d'« Editer ») | 2.9.0 | [archive](./archive/COMPLETED_v1-v2.md) |
| 102 | Assistant IA — bouton « Ajouter la section » par bloc de code (insertion du bloc seul) | 2.9.0 | [archive](./archive/COMPLETED_v1-v2.md) |
---
+19
View File
@@ -385,6 +385,25 @@ Fichiers texte non markdown :
---
## #102 — Assistant IA : « Ajouter » dans Forge + ajout d'un bloc de code ✅ TERMINÉ
Deux compléments au bouton « Ajouter » de l'assistant IA.
- **BUG-057 — Forge** : `bookslm.js::_insertIntoEditor()` ne ciblait que
`state.editorView` (CodeMirror de « Editer ») et affichait « Aucun document ouvert
dans l'éditeur » en Forge. Il prend désormais en charge les trois surfaces :
CodeMirror, l'iframe Forge (délégation par `postMessage({ type: 'parent-insert' })`,
insert au curseur via `insertAtCursor` côté `editor-poc.html`) et le textarea de
repli.
- **#102 — Ajout d'un bloc** : chaque bloc de code d'une réponse reçoit un bouton
« Ajouter la section » (révélé au survol, `.bookslm-code-insert`) qui insère
uniquement le contenu du bloc (sans les délimiteurs ` ``` `), au lieu de la réponse
complète.
- **Tests** : `tests/frontend/ai.test.mjs` (+3 : Forge, textarea, bloc de code) ;
`tests/frontend/editor-inline.test.mjs` (+1 : handler `parent-insert`).
---
## Grosses fonctionnalités — fiches dédiées
| # | Feature | Version | Fiche |
+7
View File
@@ -1474,6 +1474,13 @@ 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(); }
// #102/BUG-057 — the parent AI assistant's « Ajouter » button inserts its
// answer (or a single code block) at the current cursor position.
if (e.data.type === 'parent-insert' && typeof e.data.text === 'string') {
var p = getPos();
insertAtCursor((p.s > 0 ? '\n' : '') + e.data.text);
showToast('Texte ajoute au document', 'ok');
}
// #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
+6
View File
@@ -4319,6 +4319,12 @@
Réponses formatées : titres, listes, tableaux, citations et
blocs de code.
</li>
<li data-i18n="help.assistant_insert">
Le bouton « Ajouter » (au survol d'une réponse) insère la
réponse dans le document ouvert dans l'éditeur (Editer ou
Forge) ; chaque bloc de code propose « Ajouter la section »
pour n'insérer que ce bloc.
</li>
<li data-i18n="help.assistant_links">
Les fichiers et chemins cités sont des liens : cliquez sur un
fichier pour l'ouvrir, sur un dossier pour le révéler dans
+79 -16
View File
@@ -2153,6 +2153,7 @@ class BooksLM {
bubble.className = 'bookslm-bubble assistant';
const { text, actions } = this._extractActions(msg.content || '');
bubble.innerHTML = this._renderMarkdown(text);
this._enhanceCodeBlocks(bubble);
// Plain chat has no steps block: the pending answer itself carries the
// animated indicator until the first token arrives.
if (running && !text && !actions.length) {
@@ -2234,28 +2235,90 @@ class BooksLM {
}
/**
* Append an assistant answer to the Forge editor document (Notion “Ajouter”).
* No-ops with a toast when no editor session is open.
* Append an assistant answer (or a single code block) to the document open
* in the active editor (Notion “Ajouter”). Supports the three editor
* surfaces: CodeMirror (« Editer »), the Forge iframe and the plain textarea
* fallback. No-ops with a toast when no editor session is open.
*/
_insertIntoEditor(text) {
const value = String(text == null ? '' : text);
if (!value.trim()) return;
// 1. CodeMirror editor (« Editer »).
const view = state.editorView;
if (!view || !view.state || typeof view.dispatch !== 'function') {
showToast(t('bookslm.insert_no_editor'), 'info');
if (view && view.state && typeof view.dispatch === 'function') {
try {
const at = view.state.selection.main.to;
const insert = (at > 0 ? '\n' : '') + value;
view.dispatch({
changes: { from: at, insert },
selection: { anchor: at + insert.length },
});
if (typeof view.focus === 'function') view.focus();
showToast(t('bookslm.inserted'), 'success');
} catch (e) {
console.warn('AI assistant: insert into editor failed', e);
showToast(t('bookslm.insert_no_editor'), 'error');
}
return;
}
try {
const at = view.state.selection.main.to;
const insert = (at > 0 ? '\n' : '') + text;
view.dispatch({
changes: { from: at, insert },
selection: { anchor: at + insert.length },
});
if (typeof view.focus === 'function') view.focus();
showToast(t('bookslm.inserted'), 'success');
} catch (e) {
console.warn('AI assistant: insert into editor failed', e);
showToast(t('bookslm.insert_no_editor'), 'error');
// 2. Forge editor (same-origin iframe): its buffer lives in the child
// document, so the insertion is delegated with a postMessage.
const forge = document.getElementById('forge-iframe');
if (forge && forge.contentWindow) {
try {
forge.contentWindow.postMessage({ type: 'parent-insert', text: value }, '*');
showToast(t('bookslm.inserted'), 'success');
} catch (e) {
console.warn('AI assistant: insert into Forge failed', e);
showToast(t('bookslm.insert_no_editor'), 'error');
}
return;
}
// 3. Plain textarea fallback (CodeMirror failed to load).
const ta = state.fallbackEditorEl;
if (ta && typeof ta.value === 'string') {
const start = ta.selectionStart != null ? ta.selectionStart : ta.value.length;
const end = ta.selectionEnd != null ? ta.selectionEnd : ta.value.length;
const insert = (start > 0 ? '\n' : '') + value;
ta.value = ta.value.slice(0, start) + insert + ta.value.slice(end);
const pos = start + insert.length;
if (typeof ta.setSelectionRange === 'function') ta.setSelectionRange(pos, pos);
ta.dispatchEvent(new Event('input', { bubbles: true }));
if (typeof ta.focus === 'function') ta.focus();
showToast(t('bookslm.inserted'), 'success');
return;
}
showToast(t('bookslm.insert_no_editor'), 'info');
}
/**
* #102 — Add a discreet « Ajouter » button to every fenced code block of an
* answer, so a single proposed section can be inserted instead of the whole
* reply. The button reuses the message action styling.
*/
_enhanceCodeBlocks(bubble) {
if (!bubble || typeof bubble.querySelectorAll !== 'function') return;
const blocks = bubble.querySelectorAll('pre');
blocks.forEach((pre) => {
const code = pre.querySelector('code');
if (!code) return;
const wrap = document.createElement('div');
wrap.className = 'bookslm-code-block';
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(pre);
const btn = this._actionBtn(
t('bookslm.insert_block'),
'insert',
t('bookslm.insert_block_hint'),
() => this._insertIntoEditor(code.textContent),
);
btn.classList.add('bookslm-code-insert');
wrap.appendChild(btn);
});
}
_copyText(text) {
+3
View File
@@ -1290,6 +1290,7 @@
"help.assistant_panel": "🧠 Assistant panel (BooksLM)",
"help.assistant_panel_desc": "The side assistant (floating button or a folder's context menu) answers in formatted Markdown and contextualises your directories or documents. In general mode it also knows what you are looking at: open documents, current directory, active search and recently modified files.",
"help.assistant_markdown": "Formatted answers: headings, lists, tables, quotes and code blocks.",
"help.assistant_insert": "The \"Add\" button (revealed on hover of an answer) inserts the answer into the document open in the editor (Editer or Forge); each code block offers \"Add section\" to insert just that block.",
"help.assistant_links": "Cited files and paths are links: a bare filename copies the name to the clipboard, a folder is revealed in the tree, and a file path opens it in the viewer.",
"help.assistant_sessions": "The header history icon lists past sessions (reopen or delete); “+” starts a new conversation.",
"help.assistant_agent": "The \"agent mode\" button enables tools (read, list, search); modifying actions require confirmation with a change preview.",
@@ -1785,6 +1786,8 @@
"bookslm.insert_hint": "Append the answer to the document open in the editor",
"bookslm.inserted": "Answer added to the document",
"bookslm.insert_no_editor": "No document open in the editor",
"bookslm.insert_block": "Add section",
"bookslm.insert_block_hint": "Add only this code block to the document open in the editor",
"ai.steps_count": "{count} step",
"ai.steps_count_plural": "{count} steps",
"ai.activity_thinking": "Thinking…",
+3
View File
@@ -1290,6 +1290,7 @@
"help.assistant_panel": "🧠 Panneau Assistant (BooksLM)",
"help.assistant_panel_desc": "L'assistant latéral (bouton flottant ou menu contextuel d'un dossier) répond en Markdown formaté et contextualise vos répertoires ou documents. En contexte général, il connaît aussi ce que vous voyez : documents ouverts, répertoire courant, recherche en cours et fichiers récemment modifiés.",
"help.assistant_markdown": "Réponses formatées : titres, listes, tableaux, citations et blocs de code.",
"help.assistant_insert": "Le bouton « Ajouter » (au survol d'une réponse) insère la réponse dans le document ouvert dans l'éditeur (Editer ou Forge) ; chaque bloc de code propose « Ajouter la section » pour n'insérer que ce bloc.",
"help.assistant_links": "Les fichiers et chemins cités sont des liens : un simple nom de fichier copie le nom dans le presse-papiers, un dossier est révélé dans l'arborescence, et un chemin de fichier l'ouvre dans le viewer.",
"help.assistant_sessions": "L'icône historique de l'en-tête liste les sessions passées (recharger ou supprimer) ; « + » démarre une nouvelle conversation.",
"help.assistant_agent": "Le bouton « mode agent » active les outils (lire, lister, chercher) ; les actions de modification demandent une confirmation avec aperçu des changements.",
@@ -1785,6 +1786,8 @@
"bookslm.insert_hint": "Ajouter la réponse au document ouvert dans l'éditeur",
"bookslm.inserted": "Réponse ajoutée au document",
"bookslm.insert_no_editor": "Aucun document ouvert dans l'éditeur",
"bookslm.insert_block": "Ajouter la section",
"bookslm.insert_block_hint": "Ajouter uniquement ce bloc de code au document ouvert dans l'éditeur",
"ai.steps_count": "{count} étape",
"ai.steps_count_plural": "{count} étapes",
"ai.activity_thinking": "Réflexion…",
+8
View File
@@ -9531,6 +9531,14 @@ body.popup-mode .content-area {
.bookslm-bubble.assistant { align-self: flex-start; background: transparent; padding: 0; width: 100%; max-width: 100%; color: var(--text-primary); }
.bookslm-bubble.assistant code { background: rgba(0,0,0,0.2); padding: 1px 4px; border-radius: 3px; font-size: 0.9em; }
.bookslm-bubble.assistant pre { background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; overflow-x: auto; margin: 8px 0; }
/* #102 — per-code-block “Ajouter”: a discreet button in the block's corner,
revealed on hover/focus, that inserts only this section. */
.bookslm-code-block { position: relative; }
.bookslm-code-block .bookslm-code-insert { position: absolute; top: 6px; right: 6px;
z-index: 1; background: var(--surface2); border-color: var(--border); opacity: 0;
transition: opacity 0.15s; }
.bookslm-code-block:hover .bookslm-code-insert,
.bookslm-code-block:focus-within .bookslm-code-insert { opacity: 1; }
.bookslm-sources { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.bookslm-source-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px;
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.8.4",
"version": "2.9.0",
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
"main": "patch.js",
"directories": {
+67
View File
@@ -404,6 +404,73 @@ async function main() {
panel.remove();
});
await test("add action delegates to the Forge iframe when no CodeMirror is open (BUG-057)", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [{ role: "assistant", content: "Réponse Forge" }];
b._renderMessages();
state.editorView = null;
const iframe = document.createElement("iframe");
iframe.id = "forge-iframe";
document.body.appendChild(iframe);
const posted = [];
iframe.contentWindow.postMessage = (msg) => posted.push(msg);
panel.querySelector(".bookslm-msg.assistant .bookslm-msg-action-insert").click();
assert.equal(posted.length, 1, "one postMessage to the Forge iframe");
assert.equal(posted[0].type, "parent-insert");
assert.ok(posted[0].text.includes("Réponse Forge"), "answer forwarded");
iframe.remove();
panel.remove();
});
await test("add action falls back to the plain textarea editor", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [{ role: "assistant", content: "Réponse textarea" }];
b._renderMessages();
state.editorView = null;
const ta = document.createElement("textarea");
ta.value = "Ligne existante";
document.body.appendChild(ta);
ta.setSelectionRange(ta.value.length, ta.value.length);
state.fallbackEditorEl = ta;
panel.querySelector(".bookslm-msg.assistant .bookslm-msg-action-insert").click();
assert.ok(ta.value.includes("Réponse textarea"), "answer appended to the textarea");
state.fallbackEditorEl = null;
ta.remove();
panel.remove();
});
await test("code block exposes an add-section button inserting only the block (#102)", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [{
role: "assistant",
content: "Voici la section :\n\n```markdown\n## Titre\n\nContenu\n```\n\nFin.",
}];
b._renderMessages();
const btn = panel.querySelector(".bookslm-msg.assistant .bookslm-code-insert");
assert.ok(btn, "per-block add button rendered");
const dispatched = [];
state.editorView = {
state: { selection: { main: { to: 0 } } },
dispatch: (spec) => dispatched.push(spec),
focus: () => {},
};
btn.click();
assert.equal(dispatched.length, 1, "editor dispatch called");
assert.ok(dispatched[0].changes.insert.includes("## Titre"), "block content inserted");
assert.ok(!dispatched[0].changes.insert.includes("```"), "code fences stripped");
state.editorView = null;
panel.remove();
});
await test("manual wheel scroll releases the top-pinning", async () => {
const b = new BooksLM();
const panel = b._render();
+7
View File
@@ -347,6 +347,13 @@ test("editor-poc.html reloads Forge buffer on parent-reload", () => {
assert.match(handler[1], /loadFile\(\);/);
});
test("editor-poc.html inserts assistant text on parent-insert (BUG-057)", () => {
assert.match(forgeSrc, /if \(e\.data\.type === 'parent-insert' && typeof e\.data\.text === 'string'\) \{/);
const handler = forgeSrc.match(/if \(e\.data\.type === 'parent-insert' && typeof e\.data\.text === 'string'\) \{([\s\S]*?)\n \}/);
assert.ok(handler, "parent-insert handler not found");
assert.match(handler[1], /insertAtCursor\(/);
});
test("style.css lets the inline editor fill the content area", () => {
assert.match(cssSrc, /\.editor-modal\.editor-inline-mode \{/);
assert.match(cssSrc, /\.editor-modal\.editor-inline-mode \{[\s\S]*?pointer-events: none;/);