Paperful workbench¶
A local GUI over the same verbs as the CLI. docker compose up (or
paperful serve) binds http://127.0.0.1:8765. Zotero remains the catalogue
and reader. Grab fetches to out/; Attach writes the library.
Status: landed, not tagged 1.0. Interactive Ask is on Index
(opt-in [rag] + [llm]), not the home page. A Firefox extension and hosted
SaaS are post-1.0 — see ROADMAP — Product split.
Jobs, with screenshots: Walkthroughs (first fill, grow, tidy).


Product framing¶
Two jobs sit on the default nav:
Nav |
CLI verbs |
Job |
|---|---|---|
Wanted |
|
See missing PDFs, preview fetch, grab to |
Discover |
|
Find new works (topic or person), review, add metadata parents |
Library, Activity, and System support the loop. Repair, Mirror, Index, Briefs, and Settings appear only when Advanced is on (reveal only; does not enable Scholar, Sci-Hub, or LLM).
Browser ──HTTP──► paperful serve ──► paperful.* (same as CLI)
│
├── LibraryBackend (Zotero / …)
└── Ledger: out/ + state/
CLI / MCP ─────────────────┘
Control rules match the CLI: dry-run before bulk fetch, Preview → Grab with a review token (refuse if the library changed), opt-in Scholar / Sci-Hub / LLM / browser-agent. No built-in scheduler — Check again on Discover, not cron.
Shell chrome¶
Control |
Role |
|---|---|
Logo |
Product lockup (box + Nunito wordmark) links to Wanted; fonts match the public site (Fraunces / Nunito / Source Sans 3) |
Collection chip |
Scoped collection (remembered in a cookie) |
Preset chip |
Open access ( |
Health dot |
Worst cached |
Advanced toggle |
Cookie only; reveals extra nav and form fields |
Row actions |
Icon buttons (detail drawer, set collection scope) with the action name as the tooltip |
Long lists |
Discover lists, Index threads and batches, Briefs archives, and the Repair queue fold into collapsible sections |
Layout: table-first rows, native <dialog> drawer (no embedded PDF viewer).
Long jobs return a command id; Activity subscribes to GET /v1/runs/{id}/events (SSE), with JSON poll fallback via GET /v1/runs/{id}.
Bind 127.0.0.1. docker compose up publishes 127.0.0.1:8765:8765 only.
compose.gui.yaml is a no-op shim for older -f compose.gui.yaml --profile gui invocations.
Simple loop¶
Discover — Topic: keyword search (
snowball searchdry-run queue), per-row Keep/Skip, then Preview apply → Add selected (snowball apply, metadata only). Fill PDFs opens Wanted. Briefing / Digest writebriefing.md/digest.mdon the queue (shown on the page; digests are not auto-created). Advanced can tick File collection note (--apply+ collection chip) to file a Zotero collection note taggedpaperful:frontier-briefing. Keep an eye on this saves a topic watch only when a snowball profile is chosen. People: create/delete a list, Follow (ORCID + optional backfill), add/edit/remove, resolve, run, Get suggestions (method + limit + collection) → checkbox Accept (+ optional seed date), import CSV/JSON/ORCID or saved social HTML, people briefing (inbox plus OpenAlex recent works / co-authors for pollable members; same markdown as CLIauthorwatch briefing); inbox uses the same Preview → apply pattern (authorwatch apply). Check again re-runs a saved topic watch or person list. Advanced topic watches also offer Watch briefing / Watch digest (same optional collection note).Wanted — first nav item, and
GET /lands here. With no collection, or when the library is down, an amber coach line points at System. Missing rows use miss-surface icons (hover forMISS_SURFACE_PLAIN). Held / Have use the same icon+tooltip pattern for PDF verification (doi_match,doi_mismatch,unverified,snapshot; not “% complete”). A library PDF flag with no file on the mirror (verificationmissing, “No PDF file on disk”) lands on Missing with the missing-file icon. The default tab is the first non-empty bucket (Missing → Held → Have). Preview → Grab (fetch toout/only; selected vs all) → Attach (explicit Zotero write fordoi_matchplus hand-ticks). Grab never writes the library. The Attach control stays visible; there is no “attach verified automatically” setting.Library — Nested collection collapsibles with item / missing-PDF counts; target icon sets scope (collection chip). Scoped items paginate (default 50 per page; sizes in
[ui]— seeconfig.example.toml).Activity — Command history + trust line from
last-run.json.System —
doctorrows with one next step each.
Advanced surfaces¶
Same shell; extra verbs call the same domain entrypoints as the CLI. Discover
adds snowball kinds (hybrid/doi/orcid/collection), crawl knobs (including
Twenty writeback → cfg.twenty_writeback_listings), profile save/run,
resume, briefing, frontier digest, refs gap, ingest-dois
(Preview → Apply token), authors, and packs promote.
Wanted adds attach (mismatch/short), recover (from last run:
browser_agent_miss, missing, browser_agent_not_found — same as
recover --from-last-run-mode), handoff, inbox drain, reachout,
plus Grab filters (year / type / retry / try-all / browser-agent / upgrade).
Repair Preview runs dry-run then Apply writes: lint (read-only report),
fix-metadata, dedupe, versions, attachments, ocr.
Mirror Preview/Apply: sync, snapshot, restore, cache clean.
Index: rag ingest / search when [rag] is on; cited Ask and batch Ask
(state/ask-batch/) when [rag] and [llm] are on and the index has rows.
Ask and batch support focus presets, custom prompts (inline, upload, path, or
saved under state/prompts/), item keys, types, years, and top-k; batch adds
--force and questions file upload. Extract questions (rag questions) and
Already answered? (rag answered, state/rq-answered/) use the same scope.
Collection chip is the scope. Synthesize on Index when [llm] is on.


Briefs: collection summarize / synthesize when [llm] is on.
Per-item summarize in Wanted/Library drawers (Advanced). Settings writes
config.toml; it does not enable [rag] or [llm].
Stays CLI (no GUI control): session login, doctor --guide, mid-run
EZProxy re-login, snowball approve-each, collections add, Sci-Hub source
toggles; Ask TTY multi-turn, --show-context, and named run profiles.
paperful all / named run configs stay workflows.
HTTP capability API (P0 + GUI)¶
JSON capability routes live on paperful serve (serve.py). HTML + form POSTs
are mounted by paperful.ui (mount_ui).
Method |
Path |
Behaviour |
|---|---|---|
GET |
|
|
GET |
|
Same as |
GET |
|
Collection tree |
GET |
|
|
GET |
|
GUI command record under |
GET |
|
SSE |
POST |
|
Dry-run |
POST |
|
Same as MCP |
POST |
|
Smoke / readiness for the GUI process |
POST |
|
Review token for Grab (Advanced: year/type/retry/… flags) |
POST |
|
Consume token; fetch to |
POST |
|
Review token for held PDF attach |
POST |
|
Selected keys: attach pending |
POST |
|
Review token for browser-agent recover |
POST |
|
Consume token; |
POST |
|
|
POST |
|
One-shot inbox drain |
POST |
|
Export contact rows; optional RG tabs |
POST |
|
Enqueue single-item |
GET |
|
Per-item HTML under |
POST |
|
Enqueue snowball kind (Advanced) or keyword search |
POST |
|
Save snowball profile from the topic form |
POST |
|
Dry-run refs gap pack for the collection chip |
POST |
|
Classify DOIs / refs-gap pack; review token |
POST |
|
Consume token; create parents |
POST |
|
Authors frequency (optional |
POST |
|
Promote a proposed field author pack |
POST |
|
Follow ORCID into a list ( |
POST |
|
Mark queue DOI keep/skip |
POST |
|
Re-run topic watch or person list |
POST |
|
Enqueue |
POST |
|
Enqueue snowball from a named profile |
POST |
|
Write queue |
POST |
|
Write queue |
POST |
|
Watch inbox briefing; optional collection note |
POST |
|
Watch digest; optional collection note |
POST |
|
|
POST |
|
Review token for snowball/authorwatch apply |
POST |
|
Consume token; |
POST |
|
Create authorwatch list |
POST |
|
Add ORCID (optional display name / affiliation) |
POST |
|
Remove person from list |
POST |
|
Enqueue |
POST |
|
Enqueue |
POST |
|
Enqueue |
POST |
|
Enqueue accept checked suggestions (+ optional seed date) |
POST |
|
Update member display name / affiliation |
POST |
|
Delete list ledger ( |
POST |
|
CSV/JSON/ORCID/saved social HTML upload → list ( |
POST |
|
Write list |
POST |
|
Toggle Advanced cookie |
POST |
|
Remember collection chip cookie |
POST |
|
Write allowed |
POST |
|
Dry-run repair verb + review token |
POST |
|
Consume token; apply repair verb |
POST |
|
Dry-run mirror verb + review token |
POST |
|
Consume token; apply mirror verb |
POST |
|
Enqueue cited Ask turn; redirect to |
POST |
|
Enqueue |
POST |
|
Redirect to |
POST |
|
Enqueue batch Ask; redirect |
GET |
|
|
POST |
|
Enqueue |
POST |
|
Enqueue |
GET |
|
|
POST |
|
Enqueue |
GET |
|
HTML under |
POST |
|
Enqueue |
POST |
|
Enqueue |
GET |
|
HTML under |
GET |
|
HTML under |
HTML routes: /discover, /wanted, /library, /activity, /system, plus
advanced /repair, /mirror, /index, /briefs, /settings. GET / → /wanted.
/repair/preview and /mirror/preview enqueue work and redirect with ?run=; Apply
posts review_token from that command record (stale previews return HTTP 409).
POST /index/ask enqueues a cited Ask turn (not linked from the simple shell).
Writes over HTTP use review tokens under state/gui/reviews/; stale library
fingerprints return 409. Discover Add selected / inbox apply, ingest-dois
Apply, and Wanted Grab / recover require Preview first (tokens
may bake Grab flags). Grab fetches to out/ only; Attach is a separate
labelled library write (ticked keys from Wanted, or Advanced preview then apply).
Repair / Mirror Apply require Preview first.
Keep an eye on this needs an existing snowball profile (save_watch); it
does not write an empty watch.json. Long jobs land under state/gui/commands/;
Activity streams GET /v1/runs/{id}/events (falls back to polling GET /v1/runs/{id}).
Run status (SSE)¶
Long GUI jobs write a command record under state/gui/commands/<id>.json
(queued → running → done | failed). The workbench opens
EventSource on GET /v1/runs/{id}/events (Content-Type: text/event-stream).
Each change emits:
event: status
data: {"ok":true,"id":"…","verb":"…","status":"running",…}
The payload matches GET /v1/runs/{id} (ok plus the on-disk record, including
review_token, error, and result when present). The server sends the current
record immediately, then pushes again when the ledger file changes; comment
heartbeats (: ping) appear about every 15s while the job is still active. The
stream closes after a terminal status. Missing ids return 404 JSON (not SSE).
workbench.js reloads the page on done / failed and falls back to JSON polling
if the stream errors.
Non-goals¶
Second Zotero (reader, annotations, collection drag-and-drop)
Systematic-review screening as the primary UX
Chat-over-library as the default landing
Sci-Hub on simple pages or in Preview
Replacing Zotero sync or WebDAV