feat(slides): Session-scoped decks with Save, Enhance with AI, and versioning - #450
Merged
Conversation
Read quotas were blocking necessary file inspection, not duplicate research. Keep graph/gap/search discovery caps and teach reuse in the prompt instead. Also align terminal docs_update tests with the existing Studio-only SSE mapping so the launcher package typechecks.
Replace the in-chat SlidesCard with a compact HarnessPlaceholder launcher and a slides harness kind. DeckController accepts positional #slide-n hashes so the embedded strip can navigate independently of author ids.
Adds an Apps-parity top-level "Slides" gallery for persisted decks, born
personal and promotable to team, with per-slide thumbnails.
Backend:
- slides.Service.CopyDeckTo copies a deck (manifest + all slides) between
scopes, re-keying UUIDs via the validated WriteSlide path.
- ListDocsHandler merges personal + team decks with a per-deck scope
annotation (in-memory only; no Ent schema change). Back-compat with ?scope=.
- New SlidesPublishToTeamHandler / SlidesForkToPersonalHandler + routes,
registered before the {deckSlug} param routes.
Frontend:
- SlidesView: Personal/Team sections, deck cards with scaled sandboxed
present-mode iframe thumbnails + titles, publish/fork/delete, deck detail
reusing SlidesDeckView.
- slides.ts API: listSlidesDecks, publishDeckToTeam, forkDeckToPersonal,
deleteSlidesDeck.
- Wired into TopBar nav, hash router, and App.tsx handlers.
Fix: buildPath had no 'slides' case, so the nav click fell through to
default '/chat' and silently redirected. Added the case + regression tests.
Tests: Go slides/api/store suites green; 545 frontend tests pass.
… on-demand slides skill - ASD v2 as a strict superset of v1: raw colors, gradients, rotation, line dash/arrows, preset + custom geometry, image opacity, and multi-run rich text (ast-run). HTML/PPTX exporters and the Lit runtime accept both v1 and v2. - Templates: theme tokens + title/section/content archetypes + assets, three built-ins (light-corporate, midnight, aurora) and scoped tmpl/ persistence; create_deck gains a template arg and a new slide_templates tool. - PPTX import worker (jszip + fast-xml-parser) reconstructs an ASD v2 scene or Template from an uploaded .pptx, with structured loss diagnostics. - New API routes: GET /api/docs/slides/templates and POST /api/docs/slides/import (request body bounded via http.MaxBytesReader). - Frontend: templates UI in SlidesView + slides API client + runtime updates. - Bundled on-demand slides skill (pkg/skills/builtin_content_slides.go): full authoring reference loaded via skill_lookup(slides); the always-on prompt keeps only a minimal pointer. Skill requires the user to choose a template (presented as a table) before authoring rather than silently picking one. - Renamed the template-listing tool from list_templates to slide_templates to avoid a name collision with MCP servers exposing list_templates.
…s in PPTX export The PPTX worker read prop keys the parser never emits (fontSize, bold, valign, fontFamily, margin) and never read align at all, so exported text was left-aligned, wrongly sized, non-bold, and mis-anchored vertically versus the browser/HTML render. Rewrite the text branch of worker.mjs to read the real parser keys that AstText.ts uses (size, weight, align, anchor, color/color-token, font/font-token, inset) with matching defaults (size 32, weight 400, align left, anchor top, color-token ink, font-token body-font). Add: - resolveColorToken/resolveFont helpers that resolve theme tokens against scene.theme with the same defaults as the HTML export - ptFromPx to convert authored CSS px on the 1920x1080 canvas to points (72/160, since 160 canvas units = 1 inch) - align/anchor maps to pptxgenjs align (left/center/right) and box-level valign (top/middle/bottom) Update export_pptx_test.go to use real parser keys and assert the DrawingML fidelity tokens (algn, b="1", vertical anchor, raw color).
…k) + export fixes Slides PDF sandbox rendering: - Add GetSlidesPDFBrowserManager + sandboxBrowserRequired to route slides PDF export through a dedicated per-user "slides-pdf-<user>" sandbox container when the sandbox is enabled, mirroring the Apps "app-mcp-<user>" pattern. - ExportSlidesPDFHandler decides host-vs-container before scene load, uses a 90s cold-start timeout, and enforces a strict no-fallback contract: on a missing backend/callbacks it returns 500 with the real error and never renders on host. - Reap warm slides-pdf-<user> sessions via the shared idle watchdog (10m). - Document behavior + no-fallback contract in sandbox-backends.md (§14.2). - Tests: slides_handlers_pdf_test.go (session-id derivation, host-only local manager, registered-callback wiring, no-fallback 500, sandbox-disabled path). Earlier slides export fixes (bundled): - Fix single-page export (print pagination CSS) and content dimension/cropping (align Go doc CSS and runtime-injected @media print to 20in x 11.25in). - Add slog error logging on the PDF export path. - Frontend runtime/style and deck-view updates with Vitest coverage.
…centering Three fixes to make PPTX export match the HTML/PDF renders: 1. Spurious rectangle around content (shape border): the `shape` case forced a 1pt 172033 outline on every shape, so fill-only panels (e.g. a full-slide background/frame) gained a faint rectangle. Now only draw an outline when the deck authored one (line color, width, dash, or arrow end); otherwise emit an explicit transparent line so nothing is drawn or inherited. Authored borders are preserved. 2. Alignment short forms: decks author align as `l|ctr|r` (the slides skill's vocabulary), passed through verbatim by the parser. The PPTX alignMap only knew the full CSS words, so align="ctr" fell through to left. Extend alignMap to accept l/ctr/c/r. Also fix the HTML/PDF runtime (AstText): it set `text-align` to the raw `ctr` (invalid CSS -> left); add alignToCSS() to map ctr/l/r -> center/left/right so all formats center identically. 3. Off-center content (root cause of the ~5% left shift): the scene canvas is 1920x1080 at 160 units/inch == 12in x 6.75in, but the worker used pptxgenjs LAYOUT_WIDE (13.333in x 7.5in), placing 12in content on a wider slide with all slack on the right/bottom. Define a custom ASTONISH_CANVAS layout of exactly 12in x 6.75in so element geometry (and centering) maps 1:1 to HTML/PDF. Tests: TestPPTXShapeBorderMatchesAuthoring, TestPPTXTextHonorsShortFormAlignment, TestPPTXLayoutMatchesCanvasAndCenters (Go), and AstText.test.ts alignToCSS cases.
…ories - slides chat tool: list_templates returns compact summaries (name/label/ description/scope/archetype kinds) instead of full template payloads; full archetype markup/theme/assets are seeded only on create_deck - update slides skill doc to match the lightweight catalog workflow - agent: tool_categories + plan-mode gating adjustments; uncap source reads - launcher: surface builtin slides skill in the skills picker - docs: refresh docs-slides and sandbox-backends architecture notes
…zer fidelity path - Add dedicated Studio Templates area (list/delete/duplicate/recolor) with scope badges and archetype chips; wire router + App.tsx. - Backend: Service.Template/TemplateSlug, themes.ArchetypesFor, slides_templates_handlers list DTO + Delete/Duplicate/Recolor endpoints. - Fix .pptx import regression: add import path to largeEndpoints, raise maxImportPPTXBytes to 75MiB, return friendly 413 on oversize upload. - Remove pptx-automizer template-fidelity export path: revert to pure PptxGenJS exporter, drop OriginPPTX/OriginAsset/HasOrigin, remove pptx-automizer npm dep (keep jszip + pptxgenjs). Import still yields a lossy ASD template. Update docs + slides skill prose. - Tests: slides.test.ts, SlidesView.templates.test.tsx, Go handler/service tests updated; frontend 255 pass, go build/test + make lint green.
The Slides list was slow to load: every DeckCard fetched its full deck markup on mount and eagerly mounted up to 5 live sandboxed iframes, each booting the whole Lit slides runtime (custom elements + DeckController listeners + ResizeObserver + font/image waits). With N decks that meant N extra HTTP round-trips and up to N*5 runtime boots at once. - Render a single first-page thumbnail per card (captioned by deck title). - Lazy-mount that iframe via IntersectionObserver (rootMargin 200px); off- screen cards show a lightweight icon placeholder in the same aspect-video box (no layout shift). Falls back to eager render when IntersectionObserver is undefined (jsdom/tests). - Drop the per-card fetchSlidesDeck call and the MAX_THUMBS strip; remove now- unused fetchSlidesDeck/SlidesSlide imports. No backend/store/DeckManifest/present-runtime changes. Updated SlidesView test for the single-thumbnail behavior (9/9 pass); typecheck/lint/build green.
Previously the skill told the agent to 'pick sensible defaults and proceed', which led it to silently auto-select a template (e.g. midnight) instead of letting the user choose. Strengthen the rule in three places (Workflow step 1, Gathering Requirements, Quick Checklist): unless the user named a template or explicitly delegated the choice, the agent must STOP and present the list_templates options for the user to pick before calling create_deck. An inferred tone/brand is not permission to choose. Skill/prose only; verified with go build ./pkg/skills/ and go vet.
…me, slim list reads Imported corporate .pptx templates now render in full color and list fast. Fidelity (colorful slides): - Resolve master→layout background + chrome inheritance in import_worker.mjs so cover/divider layouts carry their real image/color backgrounds and accent shapes. - Classify layouts into role variants (title/section/agenda/closing/content) by layout name, labeled with the real PowerPoint layout name. - Drop the white slide-derived example-* archetypes (authored slides carry no bg). Performance: - Add DocsStore.ListDecksLite with Ent field projection (omit template_model + assets) for the Slides list path; full ListDecks retained for ListTemplates and Scene/exporters. Also: - Skill guidance, Templates UI chips, tests, and docs/architecture/docs-slides.md updated for labeled layout variants. - Fix TestExportPlatformBackup panic: remove stray "// +build tools" constraint that had been added to generated ent/org/runtime.go, which excluded its init() and left team.DefaultCreatedAt nil.
Imported-template fidelity + AI image workflow for Astonish Slides. Font rendering (import_worker.mjs): - Scale imported run font sizes to the ASD 1920x1080 canvas using the same `scale` as geometry (applied once in styleOf). A 10pt footer now emits size~15 instead of a tiny size=10 inside a 1.5x-enlarged box. - Inherit size/font for runs lacking explicit @sz/a:latin from the shape's own a:lstStyle, else master/layout p:txStyles (title/body/other), instead of falling to the hard-coded 24pt/serif default. Defensive: absent -> no-op. - Append a web-safe fallback chain to emitted fonts (e.g. "72 Brand, Aptos, Arial, sans-serif") so uninstalled corporate fonts degrade to sans-serif rather than the browser default serif. - Resolve +mj-*/+mn-* theme font tokens to concrete families (no token leak). Deck image assets (tools.go, service.go): - Add a lightweight per-deck asset catalog (ref/mime/bytes/kind hints, never data: URIs) surfaced on create_deck/get_deck via deckViewWithAssets. - New tool list_deck_assets: enumerate a deck's images and their asset-refs. - New tool add_deck_image: fetch a public https image via the SSRF-protected AssetIngestor and add it under sha256-<hex>, persisted with Service.AddDeckAsset (UpdateDeck, idempotent); returns the asset-ref for use in ast-image. Classification / picture slots (prior work, included): - Signature-first kindOf so Title-and-*/N-Columns/Q&A are flexible content, only genuine cover/divider/agenda/thank-you stay fixed chrome. - Empty picture placeholders emit fillable image drop-slots (in fillSlots), not dead neutral panels; borrowed hero images keep sample geometry/flip. - showMasterSp=0 warns only on genuine chrome loss. Tests & docs: new/updated Go tests (font-scale, list_deck_assets, add_deck_image, AddDeckAsset, classification, picture slots) and frontend tests; docs/architecture/docs-slides.md updated. All Go + web tests and lint pass.
…t-face Imported corporate templates set the deck theme to concrete brand families (e.g. "72 Brand"), but the runtime only carried the family NAME as a string - no font file was ever extracted or loaded, so the browser fell back to its default serif (Times). Font-embedding pipeline: - import_worker.mjs collectEmbeddedFonts() reads p:embeddedFontLst, resolves each variant's .fntdata via rels, strips the EOT wrapper (MTX-decompressing via mtx-decompressor) to recover TTF/OTF, and stores each face as a font:<family>:<variant> data: asset plus an "embedded-fonts" theme manifest. - fonts.go: EmbeddedFontRef + parseEmbeddedFonts; export_html.go writeFontFaces emits @font-face rules (CSP already allows font-src data:); writeThemeCSS skips the embedded-fonts key. - tools.go assetCatalog skips font: keys so fonts never leak into the image catalog or API responses. CSS-quoting fix (the actual "still Times" symptom): A family that starts with a digit ("72 Brand") is an invalid UNQUOTED CSS identifier, so the browser dropped the whole font-family declaration and fell back to serif even with the @font-face present. Both the importer (cssFontFamilyName/withFontFallback) and the runtime choke point (cssFontFamily in AstText.ts) now double-quote families that start with a digit or contain unsafe chars. The runtime fix is idempotent and also corrects decks stored before this change. Tests + docs for EOT stripping, extraction, @font-face emission, the catalog font guard, and cssFontFamily; docs-slides.md documents the pipeline and the digit-leading quoting rule.
… with slides variant picker Add a generic, reusable chat-question primitive the agent can render inline: - Backend [chat_question] event contract (persist + reload reconstruction), ask_user tool, and get_template_variant_previews; chat_runner emits the card and ends the turn so the user's click returns as a normal user message. - Frontend YesNoCard / SingleSelectCard (optional thumbnail slot) + live SlidesArchetypeThumb rendering an ast-deck miniature; TUI text-fallback parity. - Slides skill instructs ask_user (one question at a time) over plain-text lists. Fixes found in review: - Variant picker only showed one option: getTemplateVariantPreviews now matches the role family (title/title-2/...) via stripVariantSuffix, and ask_user generates unique ids/labels for empty/duplicate variant labels. - App crash on the question card: no error boundary existed, so a render throw blanked the whole SPA. Add ChatErrorBoundary around the card and guard option mapping; harden SlidesArchetypeThumb mount with try/catch. - OpenAI 400 'string too long' (~366MB): thumbnails embedded the full base64 asset map per variant into the tool result and persisted history. Drop the asset map everywhere; carry the template NAME and resolve asset-refs on the client at render time (cached listSlidesTemplates fetch).
…atic Promote the visual questionnaire to an explicit, mandatory workflow step (step 3, before authoring): the static picks (title/agenda/divider) PLUS up to 5 adaptive content questions chosen from the actual material — chart vs. table, timeline vs. list, comparison layout, emphasis/closing slides, etc. Reframe from a soft ceiling to a floor+ceiling: when the content shows a strong signal (numbers, comparison, dates/phases, process, notable length) the model MUST ask at least one adaptive question rather than defaulting to bullets, while still capping at 5 and skipping weak signals to avoid fatigue. Gate write_slide on the questionnaire and honor answers when authoring.
…on import Imported title placeholders rendered black even when the source PPTX authored them white (e.g. a title on a dark blue cover). The importer only captured text color from run-level a:rPr/a:solidFill, but empty layout title placeholders carry no runs — their color is inherited from the master <p:txStyles> titleStyle defRPr solidFill (or the shape's own <a:lstStyle> defRPr override). That inheritance was dropped, so the placeholder fell back to the renderer default (themeTokens.ink / black). - parseTxStyles: also capture defRPr solidFill color (not just size/font) - extractRuns: capture lstStyle defRPr color into localDefaults, inherit it for runs with no explicit color, and return the resolved defaults - classify: backfill placeholder style.color/size/font from the shape's own lstStyle defaults (tightest) then master txStyles when styleOf found no run — so empty title placeholders render in their authored color Other placeholders (mostly black on white slides) are unaffected because their inherited color already resolves to the same value.
… PPTX export
Two template-export regressions:
1. Image bytes: the PPTX worker required a pre-resolved props.data and hard-
failed ('image c-3 has no validated data') when a deck authored images as
<ast-image asset-ref>. Resolve the ref against scene.assets (mirroring the
HTML exporter), accept only data:image/*, and degrade an unresolvable ref to
a skipped image with a warning instead of failing the whole export.
2. Font stacks: pptxgenjs writes fontFace verbatim into <a:latin typeface>
without escaping. Imported-template theme fonts are full CSS font-family
stacks with embedded quotes (e.g. '"72 Brand", Aptos, Arial, sans-serif'),
producing malformed OOXML (typeface=""72 Brand", ...") so PowerPoint
recovered the file and blanked the affected slides. Reduce any CSS stack to a
single, quote-free family name at every fontFace path (theme, resolveFont,
per-run).
Adds TestPPTXFontStackProducesWellFormedXML asserting every OOXML part parses.
Extend ask_user with slidesTemplatePicker: for the first 'which template should I use?' question, enumerate every available template (built-in + imported) and generate one select option per template, each carrying a live cover-slide thumbnail (its first title archetype). Thumbnails resolve asset-refs on the client from the template's own asset map; no data: bytes in the tool result. Updates the slides skill and docs to drive the visual template choice.
Bake each slide to a small static PNG (320×180, ~5-15 KB) when a deck is
finished (review_deck), serve it at GET /api/docs/slides/{slug}/thumbnails/{idx},
and render it in the Slides view as an <img> with an empty placeholder fallback
— never a live ast-deck iframe render.
Schema:
- Add thumbnail_ref column to Slide entity (personal + team scopes)
- Add ThumbnailRef to store.SlideContent, wire through UpsertSlide/fill*Slide
- Ent auto-migrates at boot; no SQL migration files needed
Backend:
- pkg/docs/slides/deck_thumbnails.go: GenerateDeckThumbnails baker (idempotent,
best-effort, nil-browser safe, injectable renderer for tests)
- pkg/pdfgen/chrome.go: add Scale field to ScreenshotOptions; Scale < 1 reduces
the viewport dimensions and uses viewport-only capture (not fullPage) so the
runtime's DeckController scales the fixed canvas into the smaller viewport
- Versioned asset key (slidethumb/v2/<pos>) so existing full-res thumbnails
re-bake to the small format on next review_deck
- Same 1/6 downscale applied to template archetype thumbnails
- GET /api/docs/slides/{deckSlug}/thumbnails/{idx} endpoint with immutable cache
- Trigger: maybeEmitDocsUpdate fires bakeDeckThumbnails in a goroutine on
ActionDeckReviewed (new constant); uses canonical sandbox/local browser pattern
- Deck finishing never fails when browser is unavailable
Frontend:
- SlidesView.tsx SlideThumbnail: <img> from deckSlideThumbnailUrl with onError
placeholder (no live render fallback); removed pointer-events-none
- web/src/api/slides.ts: thumbnailRef field + deckSlideThumbnailUrl helper
- Tests: SlidesView renders img not iframe, onError shows placeholder
Docs:
- docs/architecture/docs-slides.md: 'Static deck slide thumbnails' section
Add a thumbnail_ready boolean to the Deck entity (personal + team scopes). GenerateDeckThumbnails sets it to true after baking. The list DTO now carries thumbnailReady so the frontend's SlideThumbnail component can skip the <img> entirely when no thumbnail exists — showing the placeholder icon immediately without issuing 5+ wasted 404 HTTP requests that delayed the Slides view.
…rsioning
- Decks created during a chat session are tagged with session_id and
hidden from the Slides view until explicitly saved.
- Session deletion cascades to remove all session-scoped decks.
- Save button in chat promotes session decks to permanent (saved- prefix slug).
- Enhance with AI creates a session-scoped copy (source parameter on
create_deck tool) preserving theme, assets, slides, thumbnails, and
schema version from the original.
- Override-save archives the old version (DeckVersion entity, up to 5 kept).
- Version history UI in Slides detail view with restore capability.
- POST /api/docs/slides/{slug}/save endpoint with new-deck and override flows.
- GET /api/docs/slides/{slug}/versions and POST .../restore endpoints.
- Thumbnail baking triggered after save for immediate preview display.
- Empty-deck present endpoint returns friendly placeholder instead of 500.
- Slug collision handling in source-copy path (deletes stale session decks,
appends hash suffix to avoid permanent deck collisions).
Schema changes:
- Added session_id, version, source_slug fields to Deck entity (personal+team).
- Added DeckVersion entity for version snapshots.
- Added ThumbnailReady to CreateDeck store method.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Apps-like lifecycle for slide decks: decks created during a chat session are session-scoped (invisible in the Slides view), and must be explicitly Saved to become permanent. Saved decks can be enhanced via AI (creating a session-scoped draft copy), and override-saving archives the old version (up to 5 kept).
Changes
Schema
session_id,version,source_slugfields to Deck entity (personal + team scopes)DeckVersionentity for version snapshots (up to 5 per deck)ThumbnailReadytoCreateDeckstore methodBackend
session_idand filtered from the Slides list viewPOST /api/docs/slides/{slug}/save): Promotes session decks to permanent with optional override + versioningGET .../versions(list history) andPOST .../versions/{v}/restorecreate_decktoolsourceparam): Copies theme, assets, slides, thumbnails (ThumbnailReady + ThumbnailRef), and schema version from the originalFrontend
SlidesDeckViewwith inline title dialog (derives slug assaved-<slugified-title>)SlidesView— navigates to chat with a prompt that creates a-draftsession copy and instructs the model to modify it in-placesaveDeck,listDeckVersions,restoreDeckVersionBug Fixes
SaveDeckVersionpassing invalid non-UUID string as ID — now auto-generates UUIDCreateDeckstore method not persistingThumbnailReadyfieldThumbnailReadyand slideThumbnailRefSchemaVersionwhenTemplateModelwas presentmake ent-generate)Architecture
Testing
go build ./...✓go test ./pkg/store/... ./pkg/docs/slides/... ./pkg/api/...✓cd web && npm run typecheck && npm test✓ (621 tests)make lint✓ (0 issues)