Decks (AI presentation builder) (part 2)
Part 2 of the Decks page: what each card is told when it is generated, how the topic colour and the 13-layout library work, publishing a deck to a web link, real images and generative art, presenter mode and speaker notes, PDF, HTML and PowerPoint export, and the data model, IPC surface and key files.
What it is
This is part 2 of the Decks (AI presentation builder) page. It covers what sits behind a finished deck — how each card is briefed, coloured and laid out, how a deck reaches the web or leaves as a file, and the data model, IPC surface and key files an agent needs in order to work on it.
Where to find it
Decks is the virtual project in the left sidebar, and the first half of this page walks through enabling it and running a generation. Everything here is reached from the deck editor once a deck exists: the theme picker sits in the editor header, the Share button publishes the open deck, and the Download menu saves it as a file.
How it behaves
Cost + credentials
Generation calls the Anthropic Messages API, which rejects OAuth tokens, so the
service resolves an API-key account via resolveApiKeyForAiFeatures(). No
key → a humanized error, never a crash. Every call is logged with
trackApiCost(accountId, …) and checked against a per-day cap
(decksDailyCostCapUsd, default $5/day), summed across the whole CLOSED set of
decks billing labels — the cap gates on the SUM, so a paid decks call whose label is
NOT a member would be neither counted toward the cap nor blocked by it. Every gate
reads that pooled figure through the one pooledDecksSpendToday() helper.
The decksPremiumModel setting does two things, and the second one costs money.
It picks a stronger model for generation (default off = the cheaper model), and it
switches on the visual self-review loop for artifact slides — render the slide, let
vision judge its design, repair it, re-judge, for a bounded number of passes. So premium
spends noticeably more per artifact slide than the model upgrade alone implies. That
extra spend bills under decks-review inside the same daily cap, and the loop is
fail-open: at the cap it skips rather than throwing, so a deck still finishes. The
outline gets max_tokens: 4096 and each card 3072.
What a card is actually told
A rule in the prompt is worth nothing unless the request carries what it refers to. For a long time the doctrine told the model "you are shown the previous and next slide, so do not reuse the previous slide's skeleton" while the request sent only the card's own title, summary, intent, layout and tone. Every card was therefore written in isolation from identical inputs, and came back the same shape — which is why generated decks looked machine-made no matter how the wording was tuned. It was an information failure, not a taste failure.
Each card request now carries the deck design brief, the previous and next slide (title + archetype — cards generate in parallel, so a neighbour's finished composition may not exist yet, but its planned shape always does), the deck accent, the card's assigned archetype, and — for artifact slides only — the worked exemplars. That last one roughly doubles the per-card prompt (~3.1k → ~5.2k tokens, about +$0.31 of input on a ten-slide premium deck), which is the price of the model seeing what "good" looks like instead of only being told.
Colour: chosen for the topic, overridable by the theme
The outline picks ONE accent for the deck's subject and returns it as a six-digit hex — amber for heat, red for a diagnosis, deep green for finance. Previously the doctrine forbade this outright ("never a hardcoded colour") and the accent was welded to the theme, so every deck came out the same indigo whatever the subject.
The slide applies it as --accent: var(--deck-accent, #hex), and that indirection is
deliberate: on the default theme nothing defines --deck-accent, so the topic colour
shows; the moment a reader picks a theme, --deck-accent is injected and the theme
wins. A plain inline colour could never be reclaimed, because CSS gives an element's
own declaration precedence over anything inherited. A malformed hex is dropped and the
theme accent stands.
Presenter mode + speaker notes
Each card can carry speaker_notes. The generator now always writes them (2-4
talking-point lines that expand on the slide, never a restatement), they persist
through CARD_UPDATE, and they surface in two places:
- In the app — a notes strip under each card in the editor, editable inline.
- In the published viewer — a single-screen presenter overlay (press
Sor the dock Notes button): current slide + next-slide preview + this slide's notes- an elapsed timer. It's an in-page overlay (not a second
window.open, which is unreliable in the sandboxed Shares iframe) driven by the same slide engine.
- an elapsed timer. It's an in-page overlay (not a second
Export (PDF + HTML + PowerPoint)
A Download menu in the editor saves the deck as a file (native Save dialog, so
desktop-only — all three channels sit in web-access-ws.ts BLOCKED_CHANNELS):
- PDF — a dedicated static render (
renderDeckPrintHtml, no JavaScript — the sharedhtmlToPdfruns with JS disabled) lays every slide out as its own 16:9 page, thenhtmlToPdf(html, { preferCSSPageSize: true })prints it. It reuses the shared slide engine, so the PDF matches the on-screen deck. - HTML — saves the self-contained published viewer document as-is.
- PowerPoint (
.pptx) — see below.
All three refuse a 0-card deck and humanize a write failure; a cancelled dialog returns
{ filePath: null } and never runs the render.
Export to PowerPoint (DECK_EXPORT_PPTX)
A real downloadable presentation that opens in PowerPoint / Google Slides / Keynote. It is
image-fidelity: every slide is a full-bleed picture of the deck's OWN static print render,
so the .pptx is pixel-identical to the on-screen deck and the PDF — charts, diagrams and
artifact slides included.
The trade-off: slide text is a picture, not editable text. You can present and share the file, but you cannot edit its wording in PowerPoint. That is inherent to the approach, not a gap to be patched — editable text would be a different export.
- Render — deck-pptx-render.ts captures
each card via
renderDeckPrintHtml(deck, [card])in a hidden, JavaScript-disabled, sandboxedcapturePagewindow, so an AI-authoredartifactslide renders inert. Captures are single-flight with a 20 s per-slide timeout. Capture size is 1280×720 — deliberately not larger: a hidden window bigger than the display gets clamped by the OS, producing a short, non-16:9 image that then stretches. The capture also force-freezes the deck's entrance animations, becausecapturePageshoots one frame and would otherwise catch text mid-fade. - Package — pptx-builder.ts writes a 16:9
OOXML
.pptxusing Node's built-inzlib(adds no dependency), assembled in memory and written in ONE atomic save. - Never a hole — a slide that fails to render becomes a titled placeholder, so the
.pptxalways has exactly as many slides as the deck (deck-pptx-export.ts). - Honest about degraded output — because the render is fail-open, a fully failed export would
otherwise save a valid file full of blank placeholders and report success. The export returns a
{ rendered, total }tally, and the editor warns when any slide fell back — naming it plainly when none rendered. - Costs nothing — pure local rendering, no model call.
Locked by deck-pptx-image-fidelity in
decks-contract.md.
For agents
Testing seam (AMC_DECKS_AI_FAKE)
Launch with AMC_DECKS_AI_FAKE=1 and generation returns deterministic, schema-valid
content with no API call and no cost (mirrors AMC_WRITER_AI_FAKE). This drives
the end-to-end E2E (tests/e2e/ui/decks-flow.spec.ts) through the real create →
generate → edit → delete flow without paid calls. A separate opt-in, paid smoke
(tests/unit/decks/deck-generation-real-smoke.test.ts, gated on
AMC_DECKS_REAL_SMOKE=1 + ANTHROPIC_API_KEY) is the live-model proof; it is skipped
by default so the gate never spends money.
Data model — decks + deck_cards
Created by migration src/main/db/migrations/20260618225846-decks-tables.ts.
decks: id (PK), title, theme_id, format, status
(draft|outline|generating|ready|error), source_prompt, source_ref_json
(nullable provenance { kind, id, title, assembledAt } for a context-pulled deck;
added by 20260709200336-add-source-ref-json-to-decks.ts), is_deleted,
created_at, updated_at.
deck_cards: id (PK), deck_id, position, layout, content_json (the
validated block union), style_json, speaker_notes, is_deleted,
created_at, updated_at.
Both tables are global per install (no account_id) and soft-deleted —
every read filters AND is_deleted = 0. Card content is the small, stable block
union in deck-types.ts (heading | paragraph | bullets | image).
Card soft-delete is tri-state so undo stays exact: 0 = active, 1 =
user-deleted, 2 = cascade-deleted (hidden because its deck was deleted).
Restoring a deck un-hides only the cards its own delete cascaded (2 → 0); a card
you deleted on purpose (1) stays deleted and is never resurrected.
IPC channels (decks:*)
Defined in src/shared/ipc-channels/decks.ts; handlers in
src/main/ipc/decks-handlers.ts; Zod schemas in src/shared/ipc-schemas/decks.ts;
response types in src/shared/ipc-response-map/decks.ts; push payloads in
src/shared/push-event-schemas/decks.ts.
| Constant | Channel string | Purpose |
|---|---|---|
DECK_LIST |
decks:list |
list all non-deleted decks |
DECK_GET |
decks:get |
fetch one deck + its cards |
DECK_CREATE |
decks:create |
create a blank deck |
DECK_UPDATE |
decks:update |
patch title / theme / status |
DECK_DELETE |
decks:delete |
soft-delete a deck |
DECK_GENERATE_OUTLINE |
decks:generate-outline |
create deck shell + stage an AI outline |
DECK_GENERATE_CARDS |
decks:generate-cards |
generate + persist cards from the staged outline |
DECK_INTERVIEW_TURN |
decks:interview-turn |
one turn of the optional pre-outline interview |
DECK_ASSEMBLE_CONTEXT |
decks:assemble-context |
read a picked session/project into an editable grounded brief |
DECK_ASSEMBLE_FILE |
decks:assemble-file |
read an inserted file into an editable grounded brief |
DECK_PUBLISH |
decks:publish |
render the deck + publish it to a public Shares URL |
DECK_EXPORT_PDF |
decks:export-pdf |
download the deck as a PDF (native Save dialog, desktop-only) |
DECK_EXPORT_HTML |
decks:export-html |
download the deck as a self-contained HTML file (desktop-only) |
DECK_EXPORT_PPTX |
decks:export-pptx |
download the deck as a PowerPoint .pptx — one slide-image per card (desktop-only) |
CARD_CREATE |
decks:card-create |
add a blank card (optionally after a given card) |
CARD_UPDATE |
decks:card-update |
patch a card's layout / content / style / notes |
CARD_DELETE |
decks:card-delete |
soft-delete one card (undo via CARD_RESTORE) |
CARD_RESTORE |
decks:card-restore |
restore a user-deleted card |
CARD_REORDER |
decks:card-reorder |
reorder a deck's cards |
DECKS_CHANGED |
decks:changed |
push — list/cards changed, refetch |
DECKS_GENERATION_PROGRESS |
decks:generation-progress |
push — { deckId, done, total, status } |
Every request channel is registered in tests/integration/ipc-contract.test.ts.
Renderer
The panel is the built-in Decks plugin's virtual project (__plugin_decks__,
panelOwnsLayout). It mounts DecksView (deck list + editor split, mobile
aware). State lives in the co-located Zustand store
(src/renderer/src/features/decks/decks-store.ts): load, select,
generateOutline, generateCards, the optional interview
(startInterview / submitInterviewAnswer / skipInterviewToOutline /
cancelInterview), addCard, updateCard, deleteCard,
restoreCard, reorderCards, remove, with optimistic updates + rollback and a
usePushListener on DECKS_CHANGED / DECKS_GENERATION_PROGRESS. Cards render
through the layout name→component map in card-layouts.tsx.
Theme selection is visual: the wizard and the editor header use DeckThemePicker
(features/decks/DeckThemePicker.tsx) — a keyboard-accessible radiogroup of mini-slide
thumbnails rendered from each theme's actual tokens; it derives from DECK_THEMES, so a
new theme appears automatically. In the editor it opens as a compact popover and re-themes
the open deck instantly via the existing DECK_UPDATE { themeId } path.
Card management follows the deletion convention: a per-card delete is
recoverable and undo-toast-only (you delete cards often), while a whole-deck
delete confirms first (it drops all of that deck's cards) — both offer Undo. The
card editor's CardBlock is React.memo'd so editing one card doesn't re-render
the rest of a large deck.
Key files
| What | Where |
|---|---|
| Migration | src/main/db/migrations/20260618225846-decks-tables.ts |
| Query layer | src/main/db/queries-decks.ts |
| Generation service | src/main/services/decks/deck-generation-service.ts |
| Context-pull assemble | src/main/services/decks/deck-context-service.ts |
| Board data formatter | src/main/services/decks/deck-board-formatter.ts |
| File extractor (office/text/PDF) | src/main/services/decks/deck-file-extract.ts |
| File-source accept + size cap | src/shared/decks/deck-file-types.ts |
| Published viewer + print (Main) | src/main/services/decks/deck-render.ts (renderDeckHtml / renderDeckPrintHtml) |
| PowerPoint export orchestrator | src/main/services/decks/deck-pptx-export.ts |
| PowerPoint slide capture | src/main/services/decks/deck-pptx-render.ts |
| PowerPoint OOXML/zip builder | src/main/services/decks/pptx-builder.ts |
| Generative art engine | src/shared/decks/deck-visuals.ts |
| Shared theme palette | src/shared/decks/deck-themes.ts |
| IPC handlers | src/main/ipc/decks-handlers.ts |
| IPC channels | src/shared/ipc-channels/decks.ts |
| Shared types | src/shared/decks/deck-types.ts |
| Layout registry | src/shared/decks/layout-registry.ts |
| Gating registry | src/shared/unreleased-features.ts (decks) |
| Renderer (store + UI) | src/renderer/src/features/decks/ |
| Contract (invariants) | .claude/memory/contracts/decks-contract.md |
Related
The first half of this page covers what Decks is, how it is turned on, the two-stage generation flow and the four ways to start a deck. The plugin form described there is one of the built-in plugins, which are managed on the plugin settings page, and a deck built from a board pulls its material from the boards covered in mission control.
Last verified 2026-09-23