fix(ai): les clics ne font plus sauter la conversation au bas (BUG-059); tableaux markdown corrects dans create_pdf (#92)
This commit is contained in:
+21
-1
@@ -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).
|
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
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.10.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.10.1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,26 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.10.1] — 2026-09-17
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- **BUG-059 — Assistant IA : un clic dans la conversation faisait sauter le texte au bas de la
|
||||||
|
fenêtre** : dans une conversation ouverte (post ancré en haut), tout clic — lien de fichier,
|
||||||
|
étapes, sélection de texte — dépinait l'ancre via le gestionnaire `mousedown` et retirait le
|
||||||
|
padding d'ancre, bornant le scroll à la nouvelle hauteur max (saut au bas). Le dépintage est
|
||||||
|
désormais réservé aux vrais gestes de scroll : molette, tactile et poignée de scroll
|
||||||
|
uniquement (`isScrollbarPress`). Fichiers : `frontend/js/bookslm.js`,
|
||||||
|
`tests/frontend/ai.test.mjs`.
|
||||||
|
- **#92 — `create_pdf` de l'Assistant : tableaux mal formatés** : l'outil `create_pdf`
|
||||||
|
(génération PDF via l'assistant) utilisait un rendu reportlab simplifié sans support des
|
||||||
|
tableaux. Il passe désormais par le même pipeline que le bouton « Télécharger PDF » de la
|
||||||
|
page document (mistune + plugin `table` + WeasyPrint), avec repli automatique sur le rendu
|
||||||
|
simple si WeasyPrint n'est pas disponible (GTK absent). Fichiers :
|
||||||
|
`backend/tools/documents.py`, `tests/test_document_tools.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.10.0] — 2026-09-17
|
## [2.10.0] — 2026-09-17
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
+3
-3
@@ -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.
|
**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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -922,8 +922,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.10.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.10.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.10.0 | Dernière mise à jour : Juin 2026*
|
*Projet : ObsiGate | Version : 2.10.1 | Dernière mise à jour : Juin 2026*
|
||||||
|
|||||||
@@ -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.
|
**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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1091,8 +1091,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.10.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.10.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.10.0 | Last updated: May 2026*
|
*Project: ObsiGate | Version: 2.10.1 | Last updated: May 2026*
|
||||||
|
|||||||
+59
-18
@@ -153,24 +153,39 @@ def _markdown_to_flowables(content: str) -> list[tuple[str, str]]:
|
|||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
@tool(
|
def _render_markdown_pdf(content: str, title: str) -> bytes | None:
|
||||||
name="create_pdf",
|
"""Render markdown → HTML → PDF through the document-page pipeline.
|
||||||
description=(
|
|
||||||
"Create a .pdf document in a vault from markdown-ish content "
|
Uses the same stack as the « Download PDF » button of the document viewer
|
||||||
"(# headings + paragraphs). Use for printable deliverables."
|
(mistune with the table plugin + WeasyPrint print CSS), so tables, code
|
||||||
),
|
blocks and lists are laid out correctly. Returns ``None`` when WeasyPrint
|
||||||
input_model=PdfInput,
|
is not importable (missing GTK on some hosts) so the caller can fall back
|
||||||
risk=ToolRisk.WRITE,
|
to the simplified reportlab renderer.
|
||||||
requires_vault=True,
|
"""
|
||||||
)
|
try:
|
||||||
def create_pdf(ctx: ToolContext, params: PdfInput) -> dict[str, Any]:
|
import mistune
|
||||||
"""Render the content with reportlab and save the PDF into the vault."""
|
|
||||||
|
from backend.pdf_export import build_pdf_html, generate_pdf
|
||||||
|
|
||||||
|
renderer = mistune.create_markdown(
|
||||||
|
escape=False,
|
||||||
|
plugins=["table", "strikethrough", "footnotes", "task_lists"],
|
||||||
|
)
|
||||||
|
html = renderer(content)
|
||||||
|
return generate_pdf(build_pdf_html(html, title), title)
|
||||||
|
except Exception as e:
|
||||||
|
# WeasyPrint loads GTK lazily: a missing native library can surface at
|
||||||
|
# import OR render time. Fall back to the simple renderer either way.
|
||||||
|
logger.warning("WeasyPrint pipeline unavailable for create_pdf: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _render_reportlab_pdf(content: str, title: str) -> bytes:
|
||||||
|
"""Fallback renderer (no WeasyPrint): headings + paragraphs, no tables."""
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4
|
||||||
from reportlab.lib.styles import getSampleStyleSheet
|
from reportlab.lib.styles import getSampleStyleSheet
|
||||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
|
||||||
|
|
||||||
path = _check_extension(params.path, ".pdf")
|
|
||||||
text = params.content[:MAX_PDF_CHARS]
|
|
||||||
styles = getSampleStyleSheet()
|
styles = getSampleStyleSheet()
|
||||||
style_map = {
|
style_map = {
|
||||||
"P": styles["BodyText"],
|
"P": styles["BodyText"],
|
||||||
@@ -179,10 +194,36 @@ def create_pdf(ctx: ToolContext, params: PdfInput) -> dict[str, Any]:
|
|||||||
"H3": styles["Heading3"],
|
"H3": styles["Heading3"],
|
||||||
}
|
}
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
doc = SimpleDocTemplate(buffer, pagesize=A4, title=params.title[:200])
|
doc = SimpleDocTemplate(buffer, pagesize=A4, title=title[:200])
|
||||||
story: list[Any] = [Paragraph(saxutils.escape(params.title[:300]), styles["Title"])]
|
story: list[Any] = [Paragraph(saxutils.escape(title[:300]), styles["Title"])]
|
||||||
for style, line in _markdown_to_flowables(text):
|
for style, line in _markdown_to_flowables(content):
|
||||||
story.append(Spacer(1, 4))
|
story.append(Spacer(1, 4))
|
||||||
story.append(Paragraph(saxutils.escape(line), style_map[style]))
|
story.append(Paragraph(saxutils.escape(line), style_map[style]))
|
||||||
doc.build(story)
|
doc.build(story)
|
||||||
return _save(params.vault, path, buffer.getvalue(), params.overwrite)
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
@tool(
|
||||||
|
name="create_pdf",
|
||||||
|
description=(
|
||||||
|
"Create a .pdf document in a vault from markdown content (headings, "
|
||||||
|
"paragraphs, tables, code blocks, lists). Use for printable "
|
||||||
|
"deliverables; tables are laid out like the document-page PDF export."
|
||||||
|
),
|
||||||
|
input_model=PdfInput,
|
||||||
|
risk=ToolRisk.WRITE,
|
||||||
|
requires_vault=True,
|
||||||
|
)
|
||||||
|
def create_pdf(ctx: ToolContext, params: PdfInput) -> dict[str, Any]:
|
||||||
|
"""Render the content and save the PDF into the vault.
|
||||||
|
|
||||||
|
Primary path: mistune (tables) + WeasyPrint — identical to the viewer's
|
||||||
|
« Download PDF » export. Fallback (WeasyPrint unavailable): simplified
|
||||||
|
reportlab layout without tables.
|
||||||
|
"""
|
||||||
|
path = _check_extension(params.path, ".pdf")
|
||||||
|
content = params.content[:MAX_PDF_CHARS]
|
||||||
|
pdf_bytes = _render_markdown_pdf(content, params.title[:300])
|
||||||
|
if pdf_bytes is None:
|
||||||
|
pdf_bytes = _render_reportlab_pdf(content, params.title[:300])
|
||||||
|
return _save(params.vault, path, pdf_bytes, params.overwrite)
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.10.0"
|
version = "2.10.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.10.0"
|
version = "2.10.1"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.10.0",
|
"version": "2.10.1",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| 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-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) |
|
| 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) |
|
||||||
| 2026-09-17 | BUG-058 | Correction | `frontend/style.css`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-058** : la barre de numérotation de ligne de l'éditeur « Editer » ne suivait pas le thème — CodeMirror peint `.cm-gutters` avec des valeurs claires codées en dur (`#f5f5f5`, bordure `#ddd`), visibles en thème sombre. Correctif : le gutter dérive des variables CSS ObsiGate (`background: color-mix(in srgb, var(--text-primary) 5%, transparent)`, `color: var(--text-secondary)`, `border-right: 1px solid var(--border)`, ligne active `color-mix(… 10% …)` / `--text-primary`), donc il suit les 15 thèmes et les 4 modes. Vérifié : `editor-inline.test.mjs` 44/44 (+1), unit 9/9, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0, et Playwright sur l'instance de test (route `style.css` remplacée par le fichier local) — sombre `color(srgb 0.90 0.93 0.95 / 0.05)` + bordure `#21262d`, clair `color(srgb 0.12 0.14 0.16 / 0.05)` + bordure `#d0d7de`, plus de `rgb(245,245,245)`. | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-17 | BUG-058 | Correction | `frontend/style.css`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-058** : la barre de numérotation de ligne de l'éditeur « Editer » ne suivait pas le thème — CodeMirror peint `.cm-gutters` avec des valeurs claires codées en dur (`#f5f5f5`, bordure `#ddd`), visibles en thème sombre. Correctif : le gutter dérive des variables CSS ObsiGate (`background: color-mix(in srgb, var(--text-primary) 5%, transparent)`, `color: var(--text-secondary)`, `border-right: 1px solid var(--border)`, ligne active `color-mix(… 10% …)` / `--text-primary`), donc il suit les 15 thèmes et les 4 modes. Vérifié : `editor-inline.test.mjs` 44/44 (+1), unit 9/9, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0, et Playwright sur l'instance de test (route `style.css` remplacée par le fichier local) — sombre `color(srgb 0.90 0.93 0.95 / 0.05)` + bordure `#21262d`, clair `color(srgb 0.12 0.14 0.16 / 0.05)` + bordure `#d0d7de`, plus de `rgb(245,245,245)`. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
| 2026-09-17 | BUG-059 | Correction | `frontend/js/bookslm.js`, `tests/frontend/ai.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-059** : dans une conversation ouverte (post ancré en haut), **tout clic** dans la fenêtre de messages — lien de fichier, étapes, sélection de texte — faisait sauter toute la conversation au bas de la fenêtre. Cause : le gestionnaire `mousedown` de dépintage (prévu pour la molette/tactile/poignée de scroll) se déclenchait aussi sur un simple clic, et le retrait du padding d'ancre (`paddingBottom`) bornait le `scrollTop` à la nouvelle hauteur max → saut au bas. Correctif : helper pur `isScrollbarPress(target, clientX, container)` — un appui ne dépine que s'il vise la **poignée de scroll** (cible = conteneur + zone de gouttière droite) ; molette et tactile conservent leur comportement. Vérifié : `ai.test.mjs` 92/92 (+1), unit 9/9, validate-imports 38 modules, pytest / ruff / mypy inchangés côté backend. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.10.0 | **Dernière mise à jour :** 2026-09-17
|
> **Version :** 2.10.1 | **Dernière mise à jour :** 2026-09-17
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
|
|||||||
+25
-1
@@ -43,6 +43,25 @@ const PANEL_MIN_WIDTH = 320;
|
|||||||
const PANEL_MAX_WIDTH = 1000;
|
const PANEL_MAX_WIDTH = 1000;
|
||||||
const PANEL_WIDTH_KEY = 'obsigate-bookslm-width';
|
const PANEL_WIDTH_KEY = 'obsigate-bookslm-width';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BUG-059 — Is this pointer press a scrollbar drag (and only that)?
|
||||||
|
*
|
||||||
|
* Only genuine scroll gestures may release the top-pinning of the latest
|
||||||
|
* question (wheel / touchmove / scrollbar drag). A scrollbar drag reports the
|
||||||
|
* scrollable container itself as the event target and lands inside the
|
||||||
|
* vertical scrollbar gutter. A plain click anywhere in the content — a link,
|
||||||
|
* a file path, the steps toggle, text selection — must NOT unpin: clearing
|
||||||
|
* the anchor padding would clamp the scroll position and jump the whole
|
||||||
|
* thread to the bottom of the conversation window.
|
||||||
|
*/
|
||||||
|
export function isScrollbarPress(target, clientX, container) {
|
||||||
|
if (!container || target !== container) return false;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
if (!rect || !Number.isFinite(rect.right)) return false;
|
||||||
|
const GUTTER = 24; // conservative vertical-scrollbar width estimate
|
||||||
|
return clientX >= rect.right - GUTTER;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accent- and case-insensitive normalization used to match `/` commands and
|
* Accent- and case-insensitive normalization used to match `/` commands and
|
||||||
* `@` mentions: skill ids/labels and vault paths may contain accented
|
* `@` mentions: skill ids/labels and vault paths may contain accented
|
||||||
@@ -985,7 +1004,12 @@ class BooksLM {
|
|||||||
};
|
};
|
||||||
messagesEl.addEventListener('wheel', unpin, { passive: true });
|
messagesEl.addEventListener('wheel', unpin, { passive: true });
|
||||||
messagesEl.addEventListener('touchmove', unpin, { passive: true });
|
messagesEl.addEventListener('touchmove', unpin, { passive: true });
|
||||||
messagesEl.addEventListener('mousedown', unpin, { passive: true });
|
// BUG-059: a scrollbar drag only — a plain click on the content must
|
||||||
|
// not unpin (it would clear the anchor padding and jump the thread to
|
||||||
|
// the bottom of the window).
|
||||||
|
messagesEl.addEventListener('mousedown', (e) => {
|
||||||
|
if (isScrollbarPress(e.target, e.clientX, messagesEl)) unpin();
|
||||||
|
}, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the session / command menus when clicking elsewhere in the panel.
|
// Close the session / command menus when clicking elsewhere in the panel.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.10.0",
|
"version": "2.10.1",
|
||||||
"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.",
|
"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",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -302,6 +302,25 @@ async function main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test("isScrollbarPress: content clicks never unpin, scrollbar drags do (BUG-059)", () => {
|
||||||
|
const { isScrollbarPress } = bookslmMod;
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.getBoundingClientRect = () => ({
|
||||||
|
left: 0, right: 800, top: 0, bottom: 600, width: 800, height: 600,
|
||||||
|
});
|
||||||
|
const link = document.createElement("a");
|
||||||
|
// Click on a file link / steps toggle → must NOT unpin (the thread
|
||||||
|
// would otherwise jump to the bottom when the padding is cleared).
|
||||||
|
assert.equal(isScrollbarPress(link, 790, container), false);
|
||||||
|
// Click on blank content area → must NOT unpin either.
|
||||||
|
assert.equal(isScrollbarPress(container, 300, container), false);
|
||||||
|
// Dragging the vertical scrollbar: target is the container itself and
|
||||||
|
// the press lands in the right-edge gutter → unpin allowed.
|
||||||
|
assert.equal(isScrollbarPress(container, 792, container), true);
|
||||||
|
// Defensive: no container / no geometry → never unpin.
|
||||||
|
assert.equal(isScrollbarPress(link, 790, null), false);
|
||||||
|
});
|
||||||
|
|
||||||
await test("the placeholder render keeps the anchor (no scroll reset before first token)", async () => {
|
await test("the placeholder render keeps the anchor (no scroll reset before first token)", async () => {
|
||||||
// Regression: the assistant placeholder used to be rendered while
|
// Regression: the assistant placeholder used to be rendered while
|
||||||
// `_isLoading` was still false, so it took the "restore previous
|
// `_isLoading` was still false, so it took the "restore previous
|
||||||
|
|||||||
@@ -150,6 +150,43 @@ class TestCreatePdf:
|
|||||||
}, confirm=True)
|
}, confirm=True)
|
||||||
assert (vault / "big.pdf").exists()
|
assert (vault / "big.pdf").exists()
|
||||||
|
|
||||||
|
def test_markdown_tables_go_through_the_export_pipeline(self, vault, monkeypatch):
|
||||||
|
# The document-page pipeline (mistune tables + WeasyPrint) must be
|
||||||
|
# used when available: capture the HTML handed to the PDF generator.
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
fake = types.ModuleType("backend.pdf_export")
|
||||||
|
|
||||||
|
def fake_build(html, title, **kw):
|
||||||
|
captured["html"] = html
|
||||||
|
captured["title"] = title
|
||||||
|
return "<html>" + html + "</html>"
|
||||||
|
|
||||||
|
fake.build_pdf_html = fake_build
|
||||||
|
fake.generate_pdf = lambda html, title=None, **kw: b"%PDF-fake"
|
||||||
|
monkeypatch.setitem(sys.modules, "backend.pdf_export", fake)
|
||||||
|
out = call_tool("create_pdf", _ctx(), {
|
||||||
|
"vault": "Vault", "path": "table.pdf", "title": "Rapport",
|
||||||
|
"content": "# T\n\n| a | b |\n|---|---|\n| 1 | 2 |",
|
||||||
|
}, confirm=True)
|
||||||
|
assert out.ok
|
||||||
|
assert "<table>" in captured["html"]
|
||||||
|
assert captured["title"] == "Rapport"
|
||||||
|
assert (vault / "table.pdf").read_bytes() == b"%PDF-fake"
|
||||||
|
|
||||||
|
def test_reportlab_fallback_when_weasyprint_missing(self, vault, monkeypatch):
|
||||||
|
# sys.modules[name] = None makes `from backend.pdf_export import …`
|
||||||
|
# raise ImportError → the simplified renderer must take over.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
monkeypatch.setitem(sys.modules, "backend.pdf_export", None)
|
||||||
|
call_tool("create_pdf", _ctx(), {
|
||||||
|
"vault": "Vault", "path": "fb.pdf", "title": "T", "content": "# H\ntexte",
|
||||||
|
}, confirm=True)
|
||||||
|
assert (vault / "fb.pdf").read_bytes().startswith(b"%PDF")
|
||||||
|
|
||||||
|
|
||||||
class TestVaultSafety:
|
class TestVaultSafety:
|
||||||
def test_path_outside_vault_rejected(self, vault):
|
def test_path_outside_vault_rejected(self, vault):
|
||||||
|
|||||||
Reference in New Issue
Block a user