Reading Queue (Marketplace plugin — save links, auto-summary, read aloud)
The Marketplace-only Reading Queue plugin: saving links, pasted text and PDFs, AI summaries you can highlight, note and search, and read-aloud — plus the acceptance checklist to re-check after any change to its UI.
What it is
The Reading Queue is a place to save things to read later. Save a web link, paste some text, or pick a PDF — it summarizes the content with AI so you can skim the key points without reading the whole article. You can highlight passages, keep notes, tag and search items, and have the summary or the full article read aloud, one item or your whole unread list back-to-back.
It shows up in the sidebar as "Reading" once installed.
Where to find it
You install it from the Marketplace; after that it appears as the "Reading" item in the sidebar, next to your other projects and plugins. Everything the plugin offers lives inside that screen — a list pane and a detail pane, the composer for adding an item, the summary card above the article body, the highlight and note tools, and the play-all control for reading your unread list aloud.
How it behaves
It is a plugin, not a built-in
This is the single most important fact for anyone changing it: Reading Queue is a
first-party, Marketplace-only, sandboxed plugin living at
src/plugins/reading-queue/. It is not core app code.
- Users install it from the Marketplace. It is deliberately excluded from the app
bundle (
BUILTIN_EXCLUDEinplugin-loader.tsandscripts/build-plugins.mjs, which must mirror each other), so the core app stays lean. - 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. Same model as
Foundry (
prdstack) and RepoGuard. - It reaches the host only through
window.AgentMC— thedb,ai,http,tts,toast,shell,settings, andthemenamespaces. It cannot importsrc/renderer/**or@shared/**.
It used to be a built-in "virtual project" (__reading_queue__) gated behind a
Settings → Lab toggle. That native implementation — the renderer feature, the
main-process backend, the reading-queue:* IPC channels, the CLI routes, and the
gating — was deleted in full on 2026-07-22. There is no fallback and no Lab
toggle any more. If you are looking for ReadingQueueView, readingQueueEnabled,
or READING_QUEUE_PROJECT_ID, they are gone on purpose.
How it stores data
The plugin owns a reading_items collection declared in its manifest
(storage.collections) — the host creates and evolves the table. Deletes are soft
(is_deleted 0/1) so Undo is just flipping the flag back.
The old native table, reading_queue_items, is a tombstone: it still exists in the
database and its historical migrations are untouched, but nothing reads or writes it.
Existing rows were deliberately not migrated into the plugin — the migration was a
"start fresh" decision. The table is never dropped (the migration ledger is
additive-only, and dropping it would destroy user rows). It stays classified 'merge'
in all-table-merge.ts so a backup still round-trips those old rows.
Read-aloud and cost
Read-aloud runs on the plugin tts capability: a permission-gated bridge method whose
host-side implementation (synthesizeForPlugin) enforces a per-plugin daily spend
cap and books the cost under the plugin's own label. That cap is $5 per plugin per
calendar day, shared with the plugin's AI calls — read-aloud and generation draw on the same
five dollars — and it resets at local midnight; see
Plugin Bridge Capabilities for the full rule. The plugin receives audio bytes
and plays them itself — it does not drive the app's TTSPlayer, and the old
reading-item-<id> pseudo-session coupling no longer exists.
Behavior checklist — what "working" means
These are the acceptance criteria to re-check after any UI change.
They replace the old FR-1..17 list, which lived on this page until the migration
retargeted it to the plugin. That list was written against the native React UI — it
named SectionHeader, CenteredSpinner, Pill and max-w-prose, none of which the
sandbox can import (the-sandbox-may-not-import-core) — so each item is restated here as user-visible
behavior. The original FR numbers are kept so older notes still resolve; the range
was never contiguous, and only 1, 2, 5, 6, 9, 10, 11, 12, 13, 14 and 15 ever existed.
| # | Behavior | Where |
|---|---|---|
| FR-1 | A bare domain (theverge.com/article) is normalized to https:// before saving. Multi-line input always saves as text, never a link. A live hint reads "Saving as: Link" / "Saving as: Text" as you type. |
ui/lib/url-detect.js |
| FR-2 | For a link item, the source URL in the detail header opens in the system browser. | ui/views/detail.js |
| FR-5 | There is no permanent "summarized" pill — ready is the silent steady state. pending shows a "Summarizing" pill; failed shows a "Failed" pill beside a Retry button. |
ui/views/list.js |
| FR-6 | Both panes show a loading state while fetching, never an empty state. | ui/views/app.js, views/detail.js |
| FR-9 | The list header reads "Reading Queue" with an "N unread" subtitle. | ui/views/list.js |
| FR-10 | With no API-key account configured, a notice points at Settings → Accounts. | ui/views/list.js |
| FR-11 | Play-all reports live progress ("Stop, playing 2 of 5") and the playing row shows a pulsing dot. | ui/views/list.js, ui/styles.css |
| FR-12 | The summary renders as a hero card above the body; the full text is collapsible and starts collapsed when a ready summary exists. | ui/views/detail.js |
| FR-13 | Delete happens immediately with an Undo toast — no confirm dialog. | ui/views/app.js |
| FR-14 | Row actions reveal on keyboard focus as well as hover; Arrow Up / Arrow Down move focus between rows. | ui/views/list.js, ui/styles.css |
| FR-15 | On a narrow viewport the layout is a single-pane stack (list → tap → detail with Back), never a side-by-side split. | ui/views/app.js |
| — | The composer shows "Add (Ctrl+Enter)" and a labelled "PDF" button, and the empty state explains what the queue is for. | ui/views/list.js |
Two behaviors cannot be checked without spending money: a summary actually
reaching ready, and read-aloud actually producing audio. Verify those by hand in a
dev instance, and note the summary path needs a stored API-key account — an OAuth
login does not satisfy ai.isConfigured(), because the Messages API cannot use an
OAuth token.
For agents
How the pieces fit
| Concern | Where |
|---|---|
| Manifest | src/plugins/reading-queue/manifest.json |
| UI shell | ui/index.html, ui/plugin.js, ui/styles.css, ui/views/ |
| Data layer | ui/lib/data.js (over AgentMC.db) |
| Ingest | ui/lib/ingest.js — URL via AgentMC.http, text, PDF via vendored pdf.min.mjs |
| Summaries | ui/lib/summarize.js (AgentMC.ai.generateStructured) |
| Read-aloud | ui/lib/audio.js (AgentMC.tts.synthesize, played in the plugin's own <audio>) |
| Pure logic | ui/lib/highlights.js, url-detect.js, search.js, playqueue.js |
| Tests | tests/unit/plugins/reading-queue/ |
Notable consequences of the sandbox model:
- Search runs in JS, not SQL — the plugin
dbhas no LIKE/FTS, so the UI fetches non-deleted rows and filters them in memory (ui/lib/search.js). - SSRF protection is the host's job.
AgentMC.http.fetchenforces the policy in main; the plugin only does a cheaphttp:/https:check for UX. That bridge returns a plain serializable object, never aResponse(aResponsecannot crosscontextBridge). - PDFs are parsed in the webview with a vendored pdf.js, not in main.
- Summaries never throw — a failure writes
summary_status: 'failed'plus a humanizedsummary_error, so one bad item cannot break the list. - No credentials are reachable. The plugin detects configuration via
AgentMC.ai.isConfigured()/AgentMC.tts.isAvailable(), never by reading a key.
Changing it
Read reading-queue-contract.md
first — it holds the invariants (core-never-re-grows-a-reading-queue … stop-settles-the-in-flight-play) and the tests that enforce them. The two
that catch the most mistakes:
core-never-re-grows-a-reading-queue— core must never re-grow a Reading Queue. New capability goes in the plugin'sui/.marketplace-only-never-bundled— the bundle-exclude must stay declared in all three mirrored sites, or the plugin ships inside the app bundle.
There is no unit harness for the webview DOM; verify UI behavior by hand in the dev instance.
The pure logic and data layer are covered
by tests/unit/plugins/reading-queue/.
Related
plugin-marketplace.md covers the Marketplace this plugin is installed from and the sandbox model its UI runs inside, while plugin-bridge-capabilities.md documents the host capabilities a sandboxed plugin may reach — the AgentMC namespaces this plugin's data, summary, HTTP and read-aloud layers are built on. The invariants a change to it must never break, together with the tests that enforce them, are locked in reading-queue-contract.md.
Last verified 2026-09-28