Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Decks (AI presentation builder)

Decks is the AI presentation builder in Omniscio: describe a deck in plain language, approve the outline it returns, then get editable cards with real layouts, images and speaker notes that you can reorder, present, publish as a web link, or download as PDF, HTML or PowerPoint.

What it is

Decks is a Gamma-style AI presentation builder inside Omniscio, surfaced as a virtual project in the sidebar. You describe a deck in plain language; Claude returns an editable outline (titles + one-line summaries); you approve it; then Claude generates each card — real rich content plus a chosen layout from a fixed library — with a real narrative arc and auto speaker notes. The result is an ordered list of cards you can reorder, edit, present, publish to a shareable web link, and download as PDF, HTML, or PowerPoint (.pptx).

Slides show real images when the model supplies one, over a deterministic generative-art base (so nothing ever looks empty) — see Real images + generative art. A presenter view (notes + next slide + timer) and a static PDF / HTML / PowerPoint export round it out.

A deck is an ordered list of cards. Each card holds normal rich content (headings, paragraphs, bullets, image) plus two presentation attributes: a layout name (from the finite registry) and an optional style (accent-image position, full-bleed, vertical alignment). This is the unit the generator emits and the editor renders.

Status

Ships the data model, the two-stage AI generation pipeline (idempotent, with a cost cap + progress + resilient parsing + atomic insert; now narrative-arc prompts + auto speaker notes), the context pull (build a deck FROM a picked session/project/board: read, clean, bound, distill into an editable grounded brief, with durable source provenance), an optional pre-outline interview (a bounded Q&A that synthesizes a brief to seed the outline), the 13-layout registry + 7 themes, the full IPC surface, the renderer panel (deck list, create flow, card editor with reorder, inline edit + notes editing, add / delete / restore card, export menu), real images + deterministic generative art, a presenter overlay (notes + next slide + timer), publish-to-web (the deck-render canonical viewer via the shared slide engine), and PDF / HTML export (static print render → htmlToPdf, desktop-only). Still tracked as follow-ups: paid AI image generation (a non-Anthropic image provider), PowerPoint export, a WYSIWYG canvas editor, and editable-outline-affects-generation. Decks remains gated in-development behind the Lab flag.

Where to find it

Decks lives in the left sidebar as its own virtual project, so there is no panel to hunt for — turning it on adds the row, and turning it off removes it. This section covers how it is enabled, how it can also run as an isolated plugin island, and what that plugin form can and cannot do compared with the built-in panel.

How it is gated

Decks is a first-party built-in plugin (src/plugins/decks/manifest.json, plugin.id: 'decks'), enabled or disabled in Settings → Plugins like any other plugin. It is off by default (enabledPlugins starts empty) and is NOT gated by an unreleased-feature flag.

Enabling it creates the __plugin_decks__ sidebar row (via the standard ensurePluginSidebarRow) and mounts the native DecksView; disabling it hides the row. The main-process CLI read routes mirror the same state — they 403 unless settings.enabledPlugins includes 'decks'. The panel renders natively (not as a webview) through the DECKS_PLUGIN_ID special-case in the dashboard layouts (mirroring NightyTidy 2); the manifest declares a ui.entryPoint but no island bundle is committed in this repo (see the bundle note below), so the native panel is all that renders on this path. Invariants: decks-plugin-contract.md.

The webview flip (decks-webview) — ON by default since 2026-07-30, with the toggle still live. A second, independent toggle routes the Decks panel to the standalone sandboxed webview island (the "detach") instead of the native panel, so Decks can be exercised as a real plugin. It is the decks-webview unreleased feature — setting decksWebviewEnabled, revealed via Settings → Lab or AMC_SHOW_DECKS_WEBVIEW=1 — and it defaults ON, so a default user gets the island and turning the setting off is what returns you to the native panel. It used to be devOnly (hard-hidden in a packaged build); that hard-hide was dropped once a packaged smoke proved the real ui/ bundle ships and a CDP probe proved it mounts and renders, so the toggle now appears in a normal install. ⚠️ The webview and the native panel read DIFFERENT data stores, so switching either way makes the other side's decks appear to vanish — they are NOT deleted, and switching back restores them. Desktop-only: the flag has no mobile consumer and an Electron <webview> cannot run on the mobile web client, so the toggle shows on a phone but changes nothing there. Invariants the-native-path-remains-the-revert / reachable-in-a-package-is-not-enabled in the same contract.

What the plugin path can and cannot do (feature parity, 2026-07-29). The island is NOT yet a like-for-like replacement for the native panel, and the gap matters if Decks is published to the marketplace. SIX host capabilities are exposed to it as bridge calls, so it WRAPS the host implementation instead of re-implementing it: decks.measureArtifact (the deterministic overflow gate that repairs text clipped past the slide frame — but only up to a point, see below), decks.renderHtml (the deck→HTML renderers — what makes Export PDF / Export HTML / Publish work at all; before it landed all three threw, while their mocked tests stayed green), decks.exportPptx (PowerPoint export, added 2026-07-29), decks.assembleFile (extract + distill a document), decks.researchTopic (the web-search grounding pass) and decks.assembleBoardContext (read + distill a shared board, added 2026-07-29). All six answer a discriminated { ok: true, … } | { ok: false, reason } so a failure can never be misread as a clean result — see plugin-bridge-hardening-contract.md measureartifact-reports-whether-it-measured / renderhtml-reports-whether-it-rendered and decks-plugin-contract.md the-host-writes-the-powerpoint-file (I34, in the export/publish contract) / I35.

exportPptx is the one that SAVES rather than returning data — a .pptx is a binary zip and the generic export.saveFile writes utf-8 only, so the host pops the Save dialog and writes the file, exactly as export.savePdf does. It carries a THIRD arm the others do not need: a user who cancels the dialog gets { ok: true, saved: false }, because a cancel genuinely ran and genuinely did not fail. On a real save it reports rendered / total, and the plugin tells you when the saved deck came back SHORT of real slides — the per-slide render is fail-open by design, so a deck carrying placeholders is a real outcome, not an exotic edge.

assembleFile and researchTopic are the CREATE-side pair. Office extraction is main-process code and a PDF rides to Claude as a native document block, neither of which the text-only AI door can express; and research is a tool_choice: auto call carrying the web_search SERVER tool, which the forced-tool AI door structurally cannot issue. Both are bridged for that reason and no other.

So the island now starts a deck ALL FOUR ways — a typed prompt, from a file, a guided interview, and from your work (a past session, project or board you have shared with it) — each optionally grounded by web research, and it reaches all three export formats (PDF, web page, PowerPoint). The interview needed no new bridge method at all: it is a forced structured-output tool call, exactly the shape the existing AI door already serves.

Boards needed a SEPARATE method rather than a third value on assembleContext's sourceKind, and the reason is worth knowing because it looks like extra work for nothing: the host gates permissions per method, not per argument. A board accepted by assembleContext would have been read under the session-history permission — so a plugin holding only that permission could have read your boards. One method, one permission, one honest sentence.

"From your work" behaves differently from the built-in panel, and that difference IS the privacy model, not a limitation. The panel can list every session and project you have, because it is the app. The plugin can only list what you have explicitly shared with it — so its picker starts empty, with a "Choose what Decks can read" button that opens Omniscio's own picker. You choose there; Decks never browses your work to offer it to you. Two consequences worth knowing:

  • Sharing a project shares its sessions, but sharing one session does NOT share its project. The asymmetry is deliberate.
  • Sharing a board shares only that board. Boards sit inside a workspace, and sharing one does NOT share the others — a workspace is far wider than the thing you actually pointed at.
  • Decks never sees your messages, or your board's items. It hands Omniscio an id and gets back only a distilled summary, so the underlying content never enters the plugin. That is structural — there is no path for it, not merely a rule being followed.
  • Boards asked separately, and on purpose. Decks requests a distinct "read your boards" permission rather than reusing the session-history one, because a board is project-management data and not a conversation. Agreeing to share past sessions should never quietly hand over something else.
  • You can withdraw any of it later in Settings → Plugins, where every grant is listed together — sessions, projects and boards in one place, each labelled for what it is. A grant whose target you have since deleted still appears there, so you can always withdraw it.

ONE capability remains host-only:

  1. The AI visual-review pass is STRUCTURALLY BLOCKED. The bridge's AI door is text-only (no image parameter), and there is an unanswered spend question on top. It cannot be built as things stand, so a published listing must say so: a deck made through the plugin does not get the visual-review pass a native-panel deck gets.

Two things about the plugin's create path differ from the built-in panel, and both are deliberate. First, the plugin refuses a larger file than the panel does (~24 MB vs 25 MB): a file crosses the plugin bridge as base64 inside a 32 MiB-capped payload, so the honest ceiling is lower, and the plugin says so up front rather than failing after the read. Second, web research bills a different budget from the slides: research and file distillation come out of the deck budget, while the outline and cards come out of the plugin's AI budget. Those two can run out independently — so if research is skipped because its budget is gone, the plugin tells you at the outline review, before it spends anything on slides, and tells you again on the finished deck. "Budget used up", "found nothing useful" and "couldn't run" are three different messages, not one.

The overflow gate guarantees a MEASURED slide, not a FITTING one (2026-07-28). Its repair is bounded on purpose — 6% per pass, two passes — so it rescues a slide that is slightly too full and deliberately leaves a badly overflowing one alone. Treat it as a safety net for near-misses, not a guarantee every slide fits. This was proven on a real render, and proving it exposed a defect worth knowing about: because the bridge used to infer its violation count from "did the card change?", an unfixable slide came back unchanged and reported violations: 0 — "measured and clean", for the worst slides in the deck — while the gate logged nothing at all in that case. The host now REPORTS { status, violations, repaired } and nothing downstream infers it; the unfixable case logs; and the island tells the AUTHOR (a line on the deck-ready panel, attention-toned, non-blocking, fired only for measured-and-not-fixed). Details in decks-plugin-marketplace-publish-preconditions-detach-build-deck-contract.md's "the NATIVE panel does not tell its author about an unfixable slide" section. ⚠️ The NATIVE panel still discards those counts, so it does not yet tell its author — the one place the built-in app is now behind the plugin. The end-to-end proof is npm run decks:measure-proof; it is a script and not a vitest test because the gate needs a real BrowserWindow, which the unit lane cannot construct.

⚠️ NO island bundle is committed in this repo — the packaged tree is where one exists. src/plugins/decks/ui/ is absent from the tree: the plugin's own .gitignore excludes it with an anchored /ui/ line, and the manifest only declares the entry point (ui.entryPoint: ui/index.html). Nothing in the root build emits it either — the island is built by the plugin's own pipeline in its own repository, and the packaged app carries the result under <resources>/plugins/decks/ui/. The invariant is decks-plugin-marketplace-publish-preconditions-detach-build-deck-contract.md's committed-bundle-matches-source: no build artifact of the island is committed here, so there is no stale copy in this repo to drift and no stamp to keep. What replaces that guarantee here is the release tripwire scripts/checks/packaged-decks-bundle-smoke.mjs (npm run decks:smoke:packaged), which reads the PACKAGED tree's manifest + ui/index.html and fails rather than skips when the bundle is missing or is the inert stub.

⚠️ Do NOT verify a rebuild by grepping the bundle for identifiers. It is minified, so local names are mangled away and a bare presence check reports a SUCCESSFUL rebuild as a failure. Only what a minifier cannot safely rename survives — bridge method names read off the host API object, store keys, object properties (measureArtifact, renderHtml, overflowingCount, preferCSSPageSize). Host-side-only strings such as the gate's not-measured status never cross into the island at all, whatever the freshness. Use an OLD→NEW differential on surviving markers, never a presence check on arbitrary symbols.

Users who had the older Decks Lab toggle (decksEnabled) on are carried over to the enabled plugin by a one-time startup migration (decksEnabled → enabledPlugins), so nobody loses access on upgrade.

How it behaves

The two-stage flow is what keeps you in control: nothing is generated until you have approved an outline, and a deck can start from a typed prompt, a file you insert, a guided interview, or work you have already done.

The two-stage generation flow

Generation is deliberately two-stage so you stay in control and never pay for a full deck you didn't approve. An optional pre-outline interview (see below) can run first to enrich the prompt:

  1. Outline — decks:generate-outline { prompt, numCards, tone?, audience?, brief? } calls Claude for a structured outline ({ title, cards: [{ title, summary, intent, suggestedLayout }] }), creates the deck shell, sets its status to 'outline', and stages the outline in memory keyed by deckId. Returns { deckId, outline }. Nothing is fully generated yet. A brief (from the interview) is folded into the generation prompt so the deck is tailored to it.
  2. Cards — decks:generate-cards { deckId } reads the staged outline, sets status 'generating', generates each card's content + layout in a bounded-concurrency pass (emitting decks:generation-progress as it goes), persists the cards, and flips status to 'ready'. On failure the deck goes to 'error' and the progress push reports status: 'error'.

The staged outline is process-local (a restart mid-flow just means you re-generate the outline) — never a durable claim.

Reliability: idempotent, resilient, atomic

Both paid stages are hardened so they never double-charge or half-build a deck (see the decks contract for the test-locked invariants):

  • Idempotent generate (no double-spend). A duplicate delivery of either paid call — a double-click, a WebSocket replay, a retry — charges exactly once. The outline path dedups on a renderer-minted clientRequestId; the cards path dedups on deckId. Concurrent duplicates share one in-flight run; a failure is never cached (the next attempt re-runs).
  • Resilient parsing (one bad block never kills the deck). Card output is salvaged, not strictly rejected: an unknown layout falls back to text, a bad or over-long block is dropped or truncated to the schema caps, and a fully-empty card becomes an explicit placeholder so generation always completes. The outline only fails if not a single card is usable.
  • Atomic card insert. Generated cards are written in one transaction — a mid-batch failure rolls the whole batch back, so a deck is never left half-built.

Build from your work — the context pull

The create screen's source switch (Prompt | From a file | From a session | From a project | From a board) lets a deck start from a file you insert, from work you already did in Omniscio, or from a Mission Control board mirrored locally, instead of a blank prompt:

  1. Pick a source. The session picker lists your 50 most recent sessions (silent recipe lanes and unresolvable projects filtered out); the project picker uses the standard project select.
  2. Read my work. decks:assemble-context { sourceKind, sourceId } runs the four-stage assemble in deck-context-service.ts: READ the transcript(s) (getFullSessionHistory / listSessions — main process only), CLEAN agent noise (stripAgentNoise, the continuous-summary filter), BOUND to the decksContextTokenBudget setting (default 20k tokens; head + tail kept, middle dropped with an omission marker; project mode allocates newest-first and reports coverage — "Included N of M sessions"), then DISTILL with one metered call (cost label decks-context, same daily cap) into { suggestedTitle, suggestedAudience, brief }.
  3. Edit the brief. The response seeds an editable "What I understood from your work" textarea (≤ 8000 chars) — the faithfulness guard: you see and correct what the deck will be grounded in before anything is generated. An unusable distill falls back to a grounded no-AI brief; an empty/unknown source is a friendly error before any paid call.
  4. Generate. The edited brief flows into the EXISTING decks:generate-outline { …, brief, sourceRef } seam. The deck row persists its origin (decks.source_ref_json), and the editor shows a quiet "from source" pill. A duplicate delivery of the paid assemble dedups on clientRequestId exactly like the outline path.

From a file is the fourth source: you insert a PDF / Word / PowerPoint / Excel / CSV / text file (decks:assemble-file { filename, base64, mimeType }) and deck-file-extract.ts turns it into text — office docs via the shared extractOfficeText, text via UTF-8, and a PDF handed to Claude as a native document block (no in-process PDF parse) — which assembleFileContext runs through the SAME BOUND → DISTILL → editable-brief → outline path above, with the same decks-context cost label + cap, clientRequestId dedup, and sourceRef.kind: 'file' provenance. It reuses partitionAttachments for the type taxonomy and the renderer's readFileAsDataUrl + useFileDropZone; an unsupported / oversized / empty file is a friendly error before any paid call.

From a board is the fifth source: you pick a Mission Control board from the local SQLite mirror (decks:assemble-context { sourceKind: 'board', sourceId }) and deck-context-service.ts reads the board's groups, items, columns, and column values via the PM query layer (the pm-queries-{group,item,column}.ts leaf modules). The formatter (deck-board-formatter.ts) renders each group's items as a status-report-style document with column-type-aware formatting — status/priority resolve their display labels from the column settings JSON, dates and timelines parse their JSON structure, people use the text field. All formatters have try/catch fallbacks for corrupt JSON. Items are capped at BOARD_ITEM_LIMIT (200) with honest coverage reporting (includedItems/totalItems/truncated). The distill uses a board-specific system prompt oriented toward status reports (not the transcript prompt). The tab only appears when BOTH decksEnabled AND missionControlEnabled are on. The board picker uses a PM_LIST_BOARDS IPC channel (desktop-only, BLOCKED in WS channels since board data is local SQLite). sourceRef.kind: 'board' provenance flows through unchanged.

Invariants (grounded coverage honesty, never-dead-end fallback, cost label + cap, idempotency, durable provenance, fake-seam fidelity, the E2E lock) are named in the decks contract. The CLI route for "turn this session into a deck" is a tracked fast-follow (approval-gated billable pattern); the channel is manifest-classified feature-scoped-surface like its siblings until then.

Optional pre-outline interview

The create screen offers "Interview me first" alongside "Generate outline". It runs a short conversational Q&A before the outline so the deck reflects context only you have — audience, the single takeaway, must-includes, proof, tone.

  • One turn per question. decks:interview-turn { prompt, answers[], tone?, audience? } calls Claude with a forced emit_interview tool that returns EITHER the next question ({ done: false, question }) or a synthesized brief ({ done: true, brief }). The renderer replays it, appending each { question, answer } to answers, until done.
  • Bounded + always completes. Capped at MAX_INTERVIEW_QUESTIONS (5) — at the cap the turn is forced to finish — and any malformed / empty result falls back to a brief woven from the answers so far, so it never loops or dead-ends.
  • Seeds the outline. On done (or "Skip to outline"), the store hands the brief straight into decks:generate-outline { …, brief }; the quick prompt path is unchanged (the interview is opt-in). Each turn is a paid call, deduped on a per-turn clientRequestId and cost-labelled decks-interview under the same daily cap; the AMC_DECKS_AI_FAKE seam covers it too.

The layout registry — single source of truth

src/shared/decks/layout-registry.ts defines the finite, named layout library (DECK_LAYOUTS). The generator emits one of these names per card, and the renderer maps each name to a React component. The Zod layout enum, the generator's allowed list, and the renderer's name→component map all derive from this one array — two guard tests keep them in lockstep: layout-registry.test.ts (the registry shape) and deck-layout-registry-completeness.test.ts (every name has a renderer component and vice-versa; runs in the always-on guard lane).

Layouts (13): title, section, text, text-image-split, bullets, two-column, stat (big-number), full-bleed-image, gallery (image grid), icon-grid, timeline, comparison, quote. Each carries an intentAffinity (which content intents it suits) that biases the generator's choice. Adding a layout is one registry entry + a component in both viewers (the parity guard fails until they match) — the shared slide engine (below) means the static PDF picks it up for free.

A gallery card holds at most DECK_GALLERY_CELLS (4) images. That constant (deck-visuals.ts) is the single source of the cap, and all three consumers read it: the card-generation prompt QUOTES it to the model, the in-app preview sizes its tiles from deckGalleryArts, and the published viewer + PDF derive their cell count from it. It has to reach the model, because a gallery handed more images than the cap renders the first 4 and silently drops the rest — so an untold generator writes image prompts you pay for and nobody can ever see (real deck 912f2b95 emitted 7). The cap governs the COUNT only: the 2×2 grid is a deliberate visual constant in both viewers, so raising it is not a one-line change. Locked by deck-gallery-cap.test.ts in the always-on guard lane (gallery-cell-cap in the decks contract).

Publish to the web (DECK_PUBLISH)

A Share button in the deck editor publishes the deck as one self-contained web page anyone can open on any device. It rides the existing Share Artifacts pipeline, so a published deck also appears in the Shares panel (revoke / expire / manage like any other share) and re-sharing an unchanged deck returns the same URL (content-hash dedup — no accidental duplicates).

  • One canonical viewer + one shared engine. src/main/services/decks/deck-render.ts is the Main-process twin of the in-app React viewer: both render the same 13-layout registry and read the same shared theme palette (src/shared/decks/deck-themes.ts, promoted to shared/ so the two can't drift). The layout rendering itself lives in one pure slide engine string (SLIDE_ENGINE_JS, no DECK/document) that the browser viewer embeds AND the static PDF renderer evaluates server-side in a Node vm — so the interactive deck, the published deck, and the PDF all draw slides from ONE implementation. The same deck-layout-registry-completeness.test.ts guard locks both viewers to the registry. Output is fully self-contained (no external fonts/scripts/network) so it renders identically inside the sandboxed, opaque-origin Shares /run iframe and offline alike — 7 themes, scroll/arrow/swipe nav, tasteful entrance motion (reduced-motion safe), a fullscreen present mode, and a single-screen presenter overlay (below) baked in.
  • Security. Every card value is HTML-escaped at render time; the embedded deck JSON is <-escaped so a card containing </script> can't break out of the inline <script>; Present is gated on document.fullscreenEnabled (hidden inside the sandbox where it can't work).
  • Handler. DECK_PUBLISH { id } renders outside the publish try (a render bug is a real fault, not a masked publish failure), then calls shareArtifactService.publishArtifact({ pastedContent: html, fullBleed: true, sourceType: 'deck-publish' }). Refuses a 0-card deck and humanizes firebase-quota / upload errors; a fresh publish mirrors to Firestore + emits SHARE_UPDATED. The renderer store guards a re-entrant click with a publishing flag (the Shares dedup makes a duplicate idempotent regardless).

Real images + generative art

Image blocks now produce pixels, not an empty placeholder:

  • Real photos. When a block carries a valid http(s) url, both viewers emit a real <img> (object-cover, lazy, alt-labelled). The URL is escaped via the viewer's esc() so a stray quote can't break out of the attribute, and an onerror removes a broken image so the art beneath shows through.
  • Generative art (the always-present base). src/shared/decks/deck-visuals.ts is a deterministic, seeded mesh-gradient generator — deckArtBackground(seed) returns a plain CSS background value; the seed comes from the card's content (deckArtSeed). It is theme-independent on purpose (real media doesn't recolour when you switch deck theme) and pure (no Date/Math.random), so the published render stays byte-deterministic. Used behind image / cover / section slides, always under a scrim that keeps overlaid text legible — a deck looks designed even with zero photos. The gallery layout seeds one art per tile via deckGalleryArts(seed) (shared by both viewers + the PDF so they can't drift).

Image generation (a paid image model) is still deferred — this wave renders real URLs + procedural art; wiring an image provider is a follow-up.

Related

This page is split across two parts: part 2 covers what each card is told when it is generated, the theme colour, presenter mode and speaker notes, PDF / HTML / PowerPoint export, and the data model, IPC surface and key files behind it all. The virtual-project row itself behaves like any other sidebar entry, which is covered on the projects sidebar page, and a published deck is managed with everything else you have shared — see share artifacts.

Last verified 2026-10-06