Books (built-in plugin)
The built-in Books reader for EPUB and PDF books you own: importing a book onto the shelf, the paginated EPUB reader and its reading settings, the separate PDF path, where a book's data and reading position are stored, and how the plugin is changed.
What it is
Books is a personal reader for EPUB and PDF books you own. Import a book from your computer and it lands on a shelf; open it to read. EPUBs get a paginated reader with a table of contents and reading settings (font, size, line spacing, margins, justify, and a light/sepia/dark theme); PDFs get a page-by-page pdf.js reader. Either way it remembers your place per book. Highlights, notes and AI study tools are planned (Phases 3–4), not yet built. It shows up in the sidebar as "Books" once the app loads.
It is a plugin — but a bundled built-in, not Marketplace-only
Books is a first-party, sandboxed plugin living at src/plugins/books/, but
unlike a Marketplace-only plugin (e.g. Reading Queue) it is bundled into the
app: it is not in plugin-loader.ts's BUILTIN_EXCLUDE, so it ships and loads as a
built-in with no separate install.
- Its UI is hand-authored vanilla JS served into an isolated webview — no React, no
bundler, no build step, and no access to the app's component kit. It reaches the host
only through the
window.AgentMCbridge (itsdbnamespace); it cannot importsrc/renderer/**or@shared/**. - Strict CSP. The webview runs under
script-src 'self'with nounsafe-inline— every handler is attached withaddEventListenerinplugin.js, so no inline handler is ever needed (deliberately stricter than its sibling plugins).
Where to find it
Books appears as its own entry in the sidebar once the app loads — there is nothing to enable and no separate install, because it ships inside the app as a built-in plugin. Open it to see your shelf, import a book from your computer, then click a book to start reading.
How it behaves
Reading a book (the reader engine)
Opening a book mounts a vendored foliate-js
fork (7 modules, pinned by SHA) in the webview. The subtle part is HOW chapters are
served — same-origin, never blob::
- The main process already unpacked the EPUB at import and serves its files under a
scoped
@datapath (/plugin/books/@data/<dir>/<href>), each response carrying a strictscript-src 'none'CSP. - foliate is driven over those same-origin files:
reader-engine.jsuses foliate only to PARSE (spine, table of contents, CFI) via afetch-backed loader, then overrides eachsection.load()to return the same-origin@dataURL. foliate's native loader would mint ablob:URL per chapter, which the plugin CSP floor forbids (frame-src 'self') — and a blob doc carries no response CSP, so the same-origin path is also the stricter one. - Your place is saved as
{ sectionIndex, progression, cfi }in the book'spositioncolumn (throttled, and when you leave), and restored on open (precise CFI first, then a clamped section index). Fixed-layout EPUBs are refused with a message (that renderer is not vendored). - Reading settings (the "Aa" panel) — font, size, line spacing, margins, justify,
and a light/sepia/dark theme — become a stylesheet (
lib/typography.jscssFromPrefs, html/body!importantso a book's own CSS can't win) applied viaview.renderer.setStyles, which foliate re-applies on every chapter load. They persist in the plugin's KV storage (amc.storage, thestoragepermission) and are normalized on read, so a stale value can never break the reader.
PDFs take a separate path — they bypass foliate entirely. The importer stores the
file as book.pdf and validates it in the main process via pdf.js (page count +
metadata, with scripting and eval off); a password-protected, empty, or corrupt PDF is
refused with a friendly message and no half-imported book. The reader fetches those
bytes same-origin from the @data route (never navigating to them) and draws the
current page onto a canvas with
vendored pdf.js — one visible page
at a time, fit to width, at a capped resolution so a very large page can't exhaust
memory. Page turns use the same arrow / on-screen controls, your page is saved as
{ pdfPage } (in the same position column EPUBs use for their CFI), and the
EPUB-only contents + "Aa" controls are hidden. A PDF shows a placeholder cover on the
shelf (page-1 thumbnails are a later polish). Because Books is a built-in plugin its
CSP already allows what pdf.js needs (the same-origin worker + eval), so no CSP change
was required.
Full invariants + the tests that lock them: books-plugin-contract.md.
How it stores data
The plugin owns a books collection declared in its manifest (storage.collections)
— the host creates and evolves the table. Each row records the book's title,
author, format, on-disk dir, cover_path, spine (JSON), added_at, a
progress fraction, the exact reading position (JSON — { sectionIndex, progression, cfi } for an EPUB, { pdfPage } for a PDF), and last_opened_at. No PDF-specific
column was needed (the page count is read live from the document at open). New fields are added additively — a manifest column
plus a plugin.version bump, and the host runs ALTER TABLE ADD COLUMN on upgrade
(existing books untouched). Highlights and notes get their own collections in a later
phase.
Usage telemetry
Books is tracked in FEATURE_REGISTRY (id books, status instrumented) — importing
a book emits an imported feature_events datum so the feature's usage is counted,
like every other tracked surface. Only the anonymous action is recorded; no book
content or personal data is sent.
For agents
How the pieces fit
| Concern | Where |
|---|---|
| Manifest | src/plugins/books/manifest.json |
| UI shell | ui/index.html, ui/plugin.js, ui/styles.css |
| Shelf view | ui/views/shelf.js |
| Reader view | ui/views/reader.js (chrome + table of contents) |
| Reader engine | ui/lib/reader-engine.js (foliate glue + position) |
| PDF reader | ui/lib/pdf-reader.js (pure page/scale) + plugin.js render |
| PDF engine | ui/vendor/pdfjs/ (pinned pdf.js + VENDOR.md) |
| Book-data URLs | ui/lib/urls.js (validated @data builder) |
| Reading engine | ui/vendor/foliate-js/ (pinned fork + VENDOR.md) |
| Import (main) | src/main/services/books/import-book.ts (EPUB + PDF) |
| Data layer | ui/lib/data.js (over AgentMC.db) |
| Escaping | ui/lib/escape.js |
| Serving + MIME | src/main/services/plugin/plugin-server.ts |
Changing it
Books is a sandboxed webview plugin: new capability goes in its ui/, never in core
src/renderer/** or src/main/** (the main-process touchpoints the reader needs are
the .xhtml/.pdf content-type map in plugin-server.ts and the import parse in
import-book.ts). The pure logic + engine mapping
have node unit tests (tests/unit/plugins/books-*.test.ts); the full
open→render→navigate→resume loop is covered by tests/e2e/ui/books-reader.spec.ts, and
otherwise verify UI behavior by hand in the dev instance. Because Books is bundled (not
Marketplace-only), it must not be added to BUILTIN_EXCLUDE, or it would stop
shipping inside the app. Invariants are locked in
books-plugin-contract.md.
Related
- reading-queue.md — a sibling reading plugin, but Marketplace-only
Last verified 2026-09-23