Skip to content

feat(gallery): Gallery templates, ThemeDesign, and Mustache rendering#15

Merged
hanrw merged 83 commits intomainfrom
gallery
Apr 9, 2026
Merged

feat(gallery): Gallery templates, ThemeDesign, and Mustache rendering#15
hanrw merged 83 commits intomainfrom
gallery

Conversation

@hanrw
Copy link
Copy Markdown
Member

@hanrw hanrw commented Apr 9, 2026

Summary

  • Gallery templates — coordinated multi-screen App Store screenshot sets (hero + feature screens) with new CLI commands (gallery-templates list, gallery-templates get --preview)
  • ThemeDesign workflow — generate a ThemeDesign (palette + decorations) with 1 AI call (themes design), then apply deterministically to any number of screenshots (themes apply-design). No repeated AI calls.
  • Mustache rendering — extracted all HTML from Swift into .mustache template files using swift-mustache. GalleryHTMLRenderer only builds context dicts; all HTML, CSS custom properties (light/dark), and keyframe animations live in templates. Templates are pre-compiled at startup.

New CLI Commands

Command Purpose
asc app-shots gallery-templates list List gallery templates (multi-screen sets)
asc app-shots gallery-templates get --id <ID> [--preview] Get gallery details or HTML preview
asc app-shots themes design --id <ID> Generate ThemeDesign JSON (1 AI call)
asc app-shots themes apply-design --design <FILE> --template <ID> ... Apply ThemeDesign deterministically (no AI)
asc app-shots themes apply --theme <ID> --template <ID> ... Full AI restyle (fallback)

New REST Endpoints

Endpoint Purpose
GET /api/v1/app-shots/gallery-templates List gallery templates
POST /api/v1/app-shots/gallery/compose Render gallery with screenshots
POST /api/v1/app-shots/themes/design Generate ThemeDesign (1 AI call)
POST /api/v1/app-shots/themes/apply-design Apply ThemeDesign (deterministic)
POST /api/v1/app-shots/themes/apply Full AI restyle (fallback)

Architecture Changes

  • GalleryHTMLRenderer → context builder only (zero HTML in Swift)
  • HTMLComposer → wraps MustacheLibrary with compiled template caching
  • GalleryPalette → owns isLight detection and headlineColor derivation
  • DecorationShape.displayCharacter → model owns its display data
  • ThemeDesignApplier → re-renders through standard pipeline with overridden palette + merged decorations
  • Preview caching via GalleryHTMLRenderer.cachedPreview() for fast API responses
  • CSS custom properties in theme-vars.mustache for light/dark theming (no color values in Swift)

Web UI

  • Theme selection applies immediately to slides with preview HTML — no auto-compose required
  • Blitz plugin design() implemented via compose bridge mode: "design"

Test plan

  • swift test — 1698 tests pass (5 new for gallery-templates, 2 new for themes design/apply-design)
  • E2E CLI validation: templates list/apply/export, gallery-templates list/get, themes list/design/apply-design/apply
  • REST API validation: all 10 endpoints tested with curl
  • Multi-device templates verified: Duo Devices, Side by Side, Triple Fan render correctly
  • Smoke test script: examples/blitz-screenshots/docs/E2E-TEST.md

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added gallery template browsing with CLI commands (gallery-templates list/get) and REST endpoints (GET /app-shots/gallery-templates).
    • Added two-phase theme design workflow: generate AI-powered design once, then apply it deterministically across all screenshots (CLI: --design-only/--apply-design; REST: POST /app-shots/themes/design and POST /app-shots/themes/apply-design).
    • Improved gallery rendering and composition capabilities.
  • Documentation

    • Updated architecture and workflow documentation for new theme and gallery features.

hanrw added 30 commits April 7, 2026 19:11
- Introduce Gallery to manage app shots and readiness state
- Add AppShot class with types, headlines, badges, and trust marks
- Define GalleryTemplate and ScreenTemplate for layout structure
- Implement GalleryPalette for color scheme management
- Provide comprehensive tests for gallery components and helpers
- Make ScreenTemplate, TextSlot, DeviceSlot, Decoration, and GalleryPalette Codable
- Implement custom Codable for GalleryTemplate to encode screens as dictionary keyed by raw values
- Default GalleryTemplate screens parameter to empty dictionary for convenience
- Implement custom Decoder init for ScreenTemplate
- Add comprehensive JSON round-trip tests for gallery types
- Verify encoding and decoding of nested structures and optional properties
- Implement GalleryHTMLRenderer to produce responsive HTML panels
- Add compose method to AppShot for template-based rendering
- Extend Gallery with renderAll and renderShot for full and single shot rendering
- Include comprehensive tests for composition and rendering behavior
- define GalleryTemplateProvider and GalleryTemplateRepository protocols
- implement AggregateGalleryTemplateRepository for combined provider results
- add unit tests for repository listing and fetching templates by id
- Add GET /app-shots/gallery-templates route in AppShotsController
- Introduce GalleryTemplate Presentable and AffordanceProviding conformances
- Inject GalleryTemplateRepository in REST routes setup
- Implement GalleryTemplate.previewHTML for self-contained preview
- Add GalleryHTMLRenderer.renderPreviewPage to render full preview page
- Encode previewHTML in GalleryTemplate Codable conformance
- Extend GalleryTemplate with description and background properties
- Update Codable conformance to include new fields
- Enhance GalleryHTMLRenderer to render previews using gallery background and wireframe phone UI
- Add light/dark background detection for styling adjustments
- Render gallery preview as horizontal strip with sample content
- Use mock gallery and panels for realistic multi-panel display
- Simplify HTML output with flexible styled container
- Add tagline, body, and trustMarks properties to AppShot model
- Update GalleryHTMLRenderer to render tagline, body text, trust marks, and floating badges with adaptive colors
- Revise preview page with realistic sample content including taglines, badges, and body text
- Reduce panel width and border radius for a refined look
- Adjust body and container styles for better alignment and scrolling
- Implement JSON encoding and decoding for Gallery and AppShot models
- Add tests to verify JSON round-trip and decoding from file formats
- Enable previewHTML inclusion in GalleryTemplate JSON encoding
- Add gallery preview rendering integration tests for HTML output validation
- Exclude previewHTML from GalleryTemplate encoding and decoding
- Move previewHTML property to Gallery for unified HTML preview rendering
- Change GalleryTemplateProvider and GalleryTemplateRepository to use Gallery
- Rename related methods to listGalleries and getGallery(templateId:)
- Add encoding support for previewHTML in Gallery Codable implementation
- Remove outdated documentation comments for clarity
- Replace listGalleryTemplates with listGalleries
- Return JSON with pretty-printed, sorted keys wrapped in "data" key
- Replace GalleryTemplateRepositoryTests to return and verify Gallery instances
- Adjust mocks and expectations to use galleries linked with templates
- Rename test methods to reflect gallery-focused behavior
- Replace mock gallery creation with direct Gallery parameter
- Utilize existing template, palette, and appShots content for preview
- Replace inline gallery template setup with makeGallery helper
- Add tests for previewHTML content with various backgrounds
- Verify previewHTML inclusion in JSON encoding
- Replace mock gallery setup with decoding from gallery-templates.json
- Verify multiple galleries and their unique content
- Generate and write preview HTML files for all galleries
- Refine GalleryComposeTests to check for phone status bar presence
- Rename renderPanel to renderScreen for clarity
- Update related variables and comments from panel to screen
- Adjust badge positioning and device frame color variables
- Improve HTML structure and CSS for gallery preview and device wireframe
- Default to preset device slot if screen template device is missing for hero shots
- Improve positioning fallback logic in
- Verify hero shot includes wireframe phone placeholder "9:41"
- Replace check for absence of <img> tag with presence of placeholder text
…erer

- Add wrapPage to GalleryHTMLRenderer to generate full HTML pages
- Replace TemplateHTMLRenderer calls with GalleryHTMLRenderer for previewHTML, apply, and renderFragment
- Introduce conversion methods on ScreenshotTemplate for ScreenTemplate, GalleryPalette, and AppShot types
- Simplify ScreenDesign.previewHTML by using TemplateContent.apply()
- Replace TemplateContent with AppShot for template application
- Simplify ScreenshotTemplate to use ScreenTemplate and GalleryPalette
- Remove legacy slots and background enums for streamlined layout
- Update all rendering paths to use AppShot for consistent content handling
- Replace TemplateContent with AppShot in template apply and render tests
- Verify previewHTML contains template name
- Refactor test factories to use screenTemplate and palette instead of legacy slots
- Update JSON encoding tests for updated deviceCount behavior
- Refactor deviceCount tests for templates with and without devices
- Adjust previewHTML test to check template name instead of generic text
- Update screenshot template tableRow test to match default deviceCount of 1
- Replace single device slot with array to allow multi-device layouts
- Add backward-compatible decoding for single or multiple devices
- Update GalleryHTMLRenderer to render multiple devices side-by-side
- Replace single DeviceSlot with array in ScreenTemplate
- Improve AppShot table output and affordances commands
- Remove unused makeScreenDesign helper from test helpers
- Simplify deviceCount computation in ScreenshotTemplate
hanrw added 23 commits April 8, 2026 20:15
- Extract shared CSS building logic into `buildPageStyles`
- Use `loadPageWrapperTemplate` to unify template loading
- Simplify style injection in HTML by replacing individual style keys with combined `styles` string
- Replace HTML string assembly with template-driven rendering in GalleryHTMLRenderer
- Build full context dictionaries for screens, devices, decorations, badges, and trust marks
- Externalize HTML structure and styles to template files, enabling easier customization
- Update resources to formatted, multiline HTML templates with semantic elements
- Maintain backward-compatible rendering methods for text slots (tagline, headline, subheading)
…ilding

- Extract reusable context builders for text, badges, decorations, and devices
- Replace inline HTML string construction with template rendering
- Add new HTML templates for subcomponents and page wrapper
- Refactor page style building and preview rendering for flexibility and clarity
- Introduce derived color properties in GalleryPalette for consistent theming
- Update GalleryHTMLRenderer to use palette colors instead of inline logic
- Move decoration shape display characters and animation CSS into domain models
- Simplify device context rendering with palette-based styling and wireframe colors
…contexts

- Replace hardcoded colors with CSS custom properties in HTML templates
- Remove GalleryPalette from context builders, passing minimal data only
- Add data-theme and themeVars for light/dark mode styling in GalleryHTMLRenderer
- Simplify decoration, badge, device, and trust mark contexts for cleaner theming
- Update wireframe and device templates to use CSS variables and modern styles
- Remove explicit HTML element render functions, relying on templates only
- Update device rendering to use context flags for phone frames and wireframes
- Simplify headline, tagline, badges, trust marks, and decorations rendering logic
- Consolidate test helpers and improve test coverage for layout and palette handling
- Inline container styling and data-theme attribute in templates for theme consistency
- Replace CSS string building in Swift with context dictionary
- Update HTML template to handle dynamic styles and viewport fill
- Share page wrapper context with ThemedPage for consistency
- Enhance block matching to correctly parse nested if tags
- Add tests covering various nested if scenarios
- Inline and optimize gallery HTML templates for performance
- Inject theme variables in preview page and screen templates
- Refactor The
- Add thread-safe in-memory cache for loaded templates
…ation

- Replace manual template parsing with MustacheTemplate compilation
- Load and cache .mustache templates from bundle resources in MustacheLibrary
- Update GalleryHTMLRenderer to render using Mustache templates by name
- Rename HTML templates from .html to .mustache and adjust markup for Mustache syntax
- Simplify context values to use booleans instead of string flags
- Update tests to reflect new section/tag syntax and rendering behavior
- Move HTML structure from Swift to 13 external `.html` files
- Introduce `HTMLComposer` lightweight template engine with variable and control syntax
- Add `HTMLTemplateRepository` protocol for template management and plugin overrides
- Preserve existing public API of `GalleryHTMLRenderer`
- Cache rendered previewHTML for AppShotTemplate and Gallery to improve performance
- Introduce thread-safe PreviewCache in GalleryHTMLRenderer for reuse and invalidation
- Introduce ThemeDesign JSON for batching AI theme generation
- Add ThemeDesignApplier to apply cached designs without AI calls
- Use cqi units for consistent sizing across previews and exports
- Enhance GalleryHTMLRenderer to merge designs into rendering pipeline
- Extend CLI with --design-only and --apply-design options for workflows
- extract all HTML, CSS colors, and animations into 7 precompiled `.mustache` files
- move theme detection and text color logic to GalleryPalette
- add Decoration optional style fields and displayCharacter computed property
- enable immediate theme application without auto-compose step
- implement Blitz plugin `design()` for fast design→apply flow
- Introduce `themes design` to generate ThemeDesign JSON via AI
- Add `themes apply-design` to apply cached ThemeDesign to templates deterministically without AI
- Simplify `themes apply` by removing design-only and apply-design flags and logic
- Update tests to cover new subcommands and workflows
- Introduce gallery-templates subcommand with list and get features
- Support output formats and HTML preview for gallery templates
- Add AggregateGalleryTemplateRepository factory method
- Include comprehensive tests for listing and retrieving gallery templates
- Rename test to focus on background gradient presence
- Replace template name check with DOCTYPE presence validation
- Introduce `gallery-templates` commands: list and get templates with preview support
- Add `themes` commands: design generation, apply cached designs with previews
- Update command structure with new source files for templates, gallery templates, and themes
- Update folder structure for better clarity and consistency
- Adjust related test file paths to match new folder name
- Correct MARK comments to reflect updated naming conventions
- Update source, infrastructure, and test folder paths for clarity
- Adjust related documentation to reflect new directory names
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Refactors screenshot rendering into a gallery-centric Mustache pipeline, replaces ScreenshotTemplate/ScreenDesign with AppShot/Gallery models, adds ThemeDesign and deterministic application, introduces new CLI/REST gallery & theme endpoints, and adds Mustache resources plus extensive tests.

Changes

Cohort / File(s) Summary
Core Gallery Domain Models
Sources/Domain/Screenshots/Gallery/AppShot.swift, Sources/Domain/Screenshots/Gallery/Gallery.swift, Sources/Domain/Screenshots/Gallery/GalleryTemplate.swift, Sources/Domain/Screenshots/Gallery/GalleryPalette.swift, Sources/Domain/Screenshots/Gallery/ScreenLayout.swift
Added AppShot, Gallery, GalleryTemplate, GalleryPalette, and ScreenLayout types modeling multi-shot galleries, palettes, text slots, device slots, and decorations; include Codable/Presentable/Affordance conformance and helper APIs (e.g., ScreenLayout.withDecorations()).
AppShotTemplate / Template API
Sources/Domain/Screenshots/AppShotTemplate.swift, Sources/Domain/Screenshots/TemplateRepository.swift
Introduced AppShotTemplate replacing ScreenshotTemplate; updated TemplateProvider/TemplateRepository APIs to return AppShotTemplate.
Rendering & Composer
Sources/Domain/Screenshots/Gallery/GalleryHTMLRenderer.swift, Sources/Domain/Screenshots/Gallery/HTMLComposer.swift, Sources/Domain/Screenshots/Gallery/Resources/*.mustache
New Mustache-based rendering pipeline: HTMLComposer loads/compiles templates; GalleryHTMLRenderer builds context dictionaries, caches preview HTML, and renders screens/previews via 7 new .mustache templates (screen, page-wrapper, theme-vars, wireframe, keyframes, preview-page, preview-screen).
Theme Design Workflow
Sources/Domain/Screenshots/ThemeDesign.swift, Sources/Domain/Screenshots/ThemeDesignApplier.swift, Sources/Domain/Screenshots/ScreenTheme.swift
Added ThemeDesign model (palette + decorations), ThemeDesignApplier to merge/apply designs, ScreenTheme.buildDesignContext(), and new ThemeProvider/ThemeRepository design() APIs for one-shot AI design generation.
Gallery Template Repository Interfaces & Aggregate
Sources/Domain/Screenshots/Gallery/GalleryTemplateRepository.swift, Sources/Infrastructure/ScreenshotPlans/AggregateGalleryTemplateRepository.swift, Sources/Infrastructure/ScreenshotPlans/AggregateTemplateRepository.swift
Added GalleryTemplateProvider/Repository protocols and AggregateGalleryTemplateRepository actor (shared singleton); updated aggregate template repo to use AppShotTemplate.
CLI: Gallery & Themes
Sources/ASCCommand/Commands/AppShots/AppShotsGalleryTemplates.swift, Sources/ASCCommand/Commands/AppShots/AppShotsThemes.swift, Sources/ASCCommand/Commands/AppShots/AppShotsCommand.swift, Sources/ASCCommand/ClientProvider.swift
Added app-shots gallery-templates command (list/get/preview); extended app-shots themes with design and apply-design; registered new subcommand and added factory makeGalleryTemplateRepository().
Web REST Endpoints
Sources/ASCCommand/Commands/Web/Controllers/AppShotsController.swift, Sources/ASCCommand/Commands/Web/RESTRoutes.swift
Added GET /app-shots/gallery-templates, POST /app-shots/gallery/compose, POST /app-shots/themes/design, and POST /app-shots/themes/apply-design; injected galleryTemplateRepo; updated template/theme apply endpoints to use new renderer and inline data-URLs; removed background parsing helper.
Deleted Legacy Template Code
Sources/Domain/ScreenshotPlans/... (ScreenshotTemplate.swift, ScreenDesign.swift, TemplateContent.swift, TemplateHTMLRenderer.swift, LayoutMode.swift, ThemedPage.swift)
Removed legacy ScreenshotTemplate, ScreenDesign, TemplateContent, TemplateHTMLRenderer, LayoutMode, and related types and tests; replaced by gallery/AppShot/template pipeline.
Rendering & Export adjustments
Sources/ASCCommand/Commands/AppShots/AppShotsExport.swift, Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift
Adjusted HTML post-processing/PNG export replacements; switched template application to use AppShot rather than removed TemplateContent; updated CLI output columns/labels.
Decoration, Palette, Layout Enhancements
Sources/Domain/Screenshots/Gallery/ScreenLayout.swift, Sources/Domain/Screenshots/Gallery/GalleryPalette.swift
Extended Decoration with optional color, background, borderRadius, animation, added DecorationShape.label(String) and DecorationAnimation enum; GalleryPalette gained optional textColor, isLight, and headlineColor logic.
Theme Repository Aggregate
Sources/Infrastructure/ScreenshotPlans/AggregateThemeRepository.swift
Added AggregateThemeRepository.design(themeId:) to delegate one-shot ThemeDesign creation to providers.
Package & Resources
Package.swift, Package.resolved
Added swift-mustache dependency and Mustache product to Domain target; added resource copy rule for Gallery/Resources templates.
Tests & Test Helpers
Tests/.../Gallery/*, Tests/ASCCommandTests/..., updated Tests/.../MockRepositoryFactory.swift, removed legacy tests
Added comprehensive gallery/theme/template tests (many new test files covering HTMLComposer, Gallery, AppShot, ScreenLayout, GalleryHTMLRenderer, ThemeDesign, ThemeDesignApplier, CLI controllers); updated test helpers to create AppShot/AppShotTemplate/Gallery artifacts; removed old ScreenDesign and template apply tests.
Docs & UI styling
docs/features/app-shots.md, docs/features/app-shots-themes.md, apps/asc-web/command-center/css/layout.css
Updated documentation to describe Gallery-centric workflow, deterministic ThemeDesign flow, new CLI flags, and REST routes; added CSS min-width: 0; overflow: hidden to .main.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as AppShotsController
    participant GalleryRepo as GalleryTemplateRepository
    participant ScreenRenderer as GalleryHTMLRenderer
    participant Composer as HTMLComposer
    participant Templates as .mustache Files

    rect rgba(100, 150, 255, 0.5)
    Note over Client,Templates: Gallery Composition Flow
    Client->>API: POST /app-shots/gallery/compose<br/>(templateId, screenshots[])
    API->>GalleryRepo: getGallery(templateId)
    GalleryRepo-->>API: Gallery with template & palette
    loop For each screenshot in input
        API->>ScreenRenderer: renderScreen(AppShot,<br/>ScreenLayout, GalleryPalette)
        ScreenRenderer->>Composer: render(template: "screen",<br/>context: {...})
        Composer->>Templates: screen.mustache
        Templates-->>Composer: rendered fragment
        Composer-->>ScreenRenderer: fragment
        ScreenRenderer-->>API: themed HTML
    end
    API-->>Client: JSON with composed screens[]
    end

    rect rgba(100, 200, 100, 0.5)
    Note over Client,Templates: Theme Design Workflow (Two Steps)
    Client->>API: POST /app-shots/themes/design<br/>(themeId)
    API->>GalleryRepo: design(themeId) → ThemeDesign
    GalleryRepo-->>API: palette + decorations
    API-->>Client: ThemeDesign JSON

    Client->>API: POST /app-shots/themes/apply-design<br/>(design, templateId, screenshot)
    API->>GalleryRepo: getGallery(templateId)
    GalleryRepo-->>API: Gallery with ScreenLayout
    API->>ScreenRenderer: renderScreen(AppShot,<br/>screenLayout.withDecorations(design.decorations),<br/>design.palette)
    ScreenRenderer->>Composer: render templates with theme context
    Composer->>Templates: screen.mustache, theme-vars.mustache,<br/>keyframes.mustache
    Templates-->>Composer: styled + decorated HTML
    Composer-->>ScreenRenderer: complete themed fragment
    ScreenRenderer-->>API: HTML
    API-->>Client: themed HTML or PNG export
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I stitched templates with mustache thread,
Gallery frames where screenshots are led,
One design from AI, then applied with cheer—
Decorations dance, the preview's clear!
A rabbit hops, declares the refactor dear. 🎨✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gallery

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/features/app-shots.md (1)

13-472: ⚠️ Potential issue | 🟠 Major

Feature doc is missing required section structure.

This update still does not provide all required sections as explicit headings (CLI Usage, Typical Workflow, API Reference, Extending are missing).

As per coding guidelines: “Update docs/features/<feature>.md with 8 sections: CLI Usage, Typical Workflow, Architecture, Domain Models, File Map, API Reference, Testing, Extending”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/app-shots.md` around lines 13 - 472, The feature doc
docs/features/app-shots.md is missing the required top-level headings; update
the file to include the eight explicit sections: "CLI Usage", "Typical
Workflow", "Architecture", "Domain Models", "File Map", "API Reference",
"Testing", and "Extending" (you can retain "Quick Start" as a subsection under
"CLI Usage"); reorganize existing content so CLI flags and examples go under
"CLI Usage", Quick Start and workflows under "Typical Workflow", architecture
diagram under "Architecture", all model/type descriptions under "Domain Models",
the sources/tests lists under "File Map"/"Testing", and add an "API Reference"
and "Extending" section with brief placeholders if needed to satisfy the
guideline.
docs/features/app-shots-themes.md (1)

28-45: ⚠️ Potential issue | 🟡 Minor

Reorganize this feature doc to the required section contract.

This page still uses custom headings (User Journey, CLI / Agent, CLI Reference, Plugin Integration Guide) instead of the required feature-doc sections. Please rename/split these so readers can reliably find CLI Usage, Typical Workflow, API Reference, and Extending.

As per coding guidelines, docs/features/<feature>.md must be updated with 8 sections: CLI Usage, Typical Workflow, Architecture, Domain Models, File Map, API Reference, Testing, Extending.

Also applies to: 64-123, 476-515

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/app-shots-themes.md` around lines 28 - 45, Rework the document
to the required 8-section contract: replace the custom headings ("User Journey",
"Plugin UI", "CLI / Agent", "CLI Reference", "Plugin Integration Guide") with
the mandated sections: CLI Usage, Typical Workflow, Architecture, Domain Models,
File Map, API Reference, Testing, and Extending; move the "Capture → Design →
Pick Theme → Export" flow and plugin steps (including the POST /themes/design
call and the ThemeDesign / ThemeDesignApplier.apply items) into Typical Workflow
and API Reference as appropriate; extract implementation details about
deterministic re-rendering and ThemeDesign JSON into Domain Models and
Architecture; add a File Map that lists related files referenced by the doc and
a Testing section describing how to test theme application, and put integration
instructions under Extending. Ensure headings exactly match the required section
names and split/relocate content from the old headings accordingly.
🟠 Major comments (27)
Sources/ASCCommand/Commands/AppShots/AppShotsExport.swift-47-50 (1)

47-50: ⚠️ Potential issue | 🟠 Major

Make preview-container replacement resilient to non-default aspect ratios.

At Line 49, the replacement only matches aspect-ratio:1320/2868. If the preview HTML uses any other ratio (or slightly different formatting), the 320px container is not replaced, and export can render as a scaled preview instead of full-frame.

Proposed fix
-            htmlContent = htmlContent
-                .replacingOccurrences(of: "width:320px;aspect-ratio:1320/2868;container-type:inline-size", with: "width:100%;height:100%;container-type:inline-size")
+            htmlContent = htmlContent
+                .replacingOccurrences(
+                    of: #"width:\s*320px\s*;\s*aspect-ratio:[^;]+\s*;\s*container-type:inline-size"#,
+                    with: "width:100%;height:100%;container-type:inline-size",
+                    options: .regularExpression
+                )
                 .replacingOccurrences(of: "display:flex;justify-content:center;align-items:center;min-height:100vh;background:`#111`", with: "margin:0;overflow:hidden")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ASCCommand/Commands/AppShots/AppShotsExport.swift` around lines 47 -
50, The current htmlContent replacement is too specific (matches
"aspect-ratio:1320/2868") so the width:320px branch in AppShotsExport.swift (the
htmlContent.contains("width:320px") check and the replacingOccurrences(of:...)
calls) can miss other aspect ratios; update the first replacingOccurrences to
use a regular-expression pattern that matches any aspect-ratio value and
tolerates optional whitespace/order (e.g. a regex like
"width:320px;\\s*aspect-ratio:[^;]+;\\s*container-type:inline-size" or a more
permissive pattern) and call
replacingOccurrences(with:options:.regularExpression) so any aspect-ratio is
replaced with "width:100%;height:100%;container-type:inline-size"; keep the
second replacement (display:flex...) but ensure patterns tolerate minor spacing
differences as well by switching to regex where appropriate.
docs/features/app-shots.md-212-212 (1)

212-212: ⚠️ Potential issue | 🟠 Major

Dependency flow diagram conflicts with enforced architecture direction.

Line 212 documents ASCCommand → Domain ← Infrastructure, but the project rule is ASCCommand → Infrastructure → Domain.

As per coding guidelines: “Architecture must maintain three strict layers with unidirectional dependency flow: ASCCommand → Infrastructure → Domain”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/app-shots.md` at line 212, The documented dependency arrow is
inverted; update the diagram text that currently reads "ASCCommand → Domain ←
Infrastructure" so it reflects the enforced unidirectional architecture
"ASCCommand → Infrastructure → Domain" by replacing the middle and right-hand
arrows accordingly (reference symbols: ASCCommand, Infrastructure, Domain).
Sources/Domain/Screenshots/Gallery/GalleryPalette.swift-7-33 (1)

7-33: ⚠️ Potential issue | 🟠 Major

Add AffordanceProviding support to this Domain model.

GalleryPalette is a Domain model but currently lacks AffordanceProviding conformance and an affordances field.

As per coding guidelines: “All domain models must implement AffordanceProviding protocol with an affordances field containing ready-to-run CLI commands”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/GalleryPalette.swift` around lines 7 - 33,
GalleryPalette must conform to AffordanceProviding and include an affordances
field: add AffordanceProviding to the type declaration, add a public let
affordances: [Affordance] (or the project's affording type) with a default empty
value to the primary init (keep existing parameters but add affordances:
[Affordance] = []), update the custom init(from:) to
decodeIfPresent(.affordances) and fall back to [] and add .affordances to the
CodingKeys enum so Codable encoding/decoding continues to work; ensure the new
affordances property name and the AffordanceProviding protocol are referenced
exactly (GalleryPalette, affordances, AffordanceProviding, init(from:),
CodingKeys).
Sources/Domain/Screenshots/Gallery/Resources/preview-screen.mustache-1-1 (1)

1-1: ⚠️ Potential issue | 🟠 Major

Hard-coded aspect ratio will distort previews for non-portrait templates.

This template always renders 1320/2868. For landscape/square/portrait43 previews, cards will be scaled incorrectly.

Suggested fix
-<div style="width:120px;aspect-ratio:1320/2868;border-radius:6px;overflow:hidden;flex-shrink:0;box-shadow:0 1px 4px rgba(0,0,0,0.06),0 4px 12px rgba(0,0,0,0.04)">{{{screen}}}</div>
+<div style="width:120px;aspect-ratio:{{aspectRatio}};border-radius:6px;overflow:hidden;flex-shrink:0;box-shadow:0 1px 4px rgba(0,0,0,0.06),0 4px 12px rgba(0,0,0,0.04)">{{{screen}}}</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/Resources/preview-screen.mustache` at line
1, The preview template hard-codes aspect-ratio:1320/2868 which distorts
non-portrait templates; update preview-screen.mustache to accept a dynamic
aspect ratio (e.g., a template variable like {{aspectRatio}} or computed
width/height) and use that variable in place of the fixed 1320/2868 so
landscape/square/portrait43 previews scale correctly; ensure the producer code
that renders this template supplies the correct aspect ratio (derived from the
screen's width/height or template type) and consider using CSS fallbacks (e.g.,
removing the fixed aspect-ratio or adding object-fit rules for the {{{screen}}}
content) so rendering remains correct across all template orientations.
Sources/Domain/Screenshots/Gallery/GalleryPalette.swift-40-44 (1)

40-44: ⚠️ Potential issue | 🟠 Major

isLight currently misclassifies palettes and can force incorrect dark-mode behavior.

At Line 41, returning false whenever textColor exists couples text override to theme detection. Also, the substring heuristic at Line 42-43 is too loose for many valid CSS colors/gradients.

Suggested fix
 extension GalleryPalette {
+    private var firstHexColor: String? {
+        let pattern = #"#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})"#
+        guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
+        let range = NSRange(background.startIndex..<background.endIndex, in: background)
+        guard let match = regex.firstMatch(in: background, range: range),
+              let swiftRange = Range(match.range, in: background) else { return nil }
+        return String(background[swiftRange])
+    }
+
+    private func luminance(for hex: String) -> Double? {
+        var value = hex
+        if value.count == 4 { // `#RGB` -> `#RRGGBB`
+            value = "#" + value.dropFirst().map { "\($0)\($0)" }.joined()
+        }
+        guard value.count == 7 else { return nil }
+        let r = Double(Int(value.dropFirst(1).prefix(2), radix: 16) ?? 0) / 255.0
+        let g = Double(Int(value.dropFirst(3).prefix(2), radix: 16) ?? 0) / 255.0
+        let b = Double(Int(value.dropFirst(5).prefix(2), radix: 16) ?? 0) / 255.0
+        return 0.2126 * r + 0.7152 * g + 0.0722 * b
+    }
+
     /// Whether this palette has a light background (heuristic based on hex values).
     public var isLight: Bool {
-        guard textColor == nil else { return false }
-        let lightHex = ["#f", "#F", "#e", "#E", "#d", "#D", "#c", "#C", "#b", "#B", "#a8", "#A8", "#9"]
-        return lightHex.contains(where: { background.contains($0) })
+        guard let hex = firstHexColor, let luma = luminance(for: hex) else { return false }
+        return luma >= 0.6
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/GalleryPalette.swift` around lines 40 -
44, The isLight computed property should not early-return based on textColor and
should use a precise color detection and luminance check instead of loose
substring matching: in GalleryPalette.isLight remove the guard that returns
false when textColor != nil, detect an actual hex color at the start of
background (or extract the first color token from a gradient) using a
case-insensitive regex that matches valid 3/4/6/8-digit hex forms, expand
shorthand hex to full RGB, convert sRGB -> linear and compute relative luminance
from the RGB channels, then return true when that luminance exceeds a chosen
threshold (e.g. 0.5) to indicate a light background; fallback to false if no
parseable color is found.
Sources/Domain/Screenshots/Gallery/Resources/preview-page.mustache-6-7 (1)

6-7: ⚠️ Potential issue | 🟠 Major

Harden unescaped HTML/CSS injection points.

{{{themeVars}}} and {{{screenDivs}}} are raw inserts. If any upstream value is AI/user/plugin-controlled, preview HTML is vulnerable to injection. Add CSP defense-in-depth and ensure upstream sanitization/allowlisting.

🔒 Suggested hardening (defense in depth)
-<!DOCTYPE html><html><head><meta charset="utf-8">
+<!DOCTYPE html><html><head><meta charset="utf-8">
+<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: blob: https: file:; style-src 'unsafe-inline'; font-src data: https:">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/Resources/preview-page.mustache` around
lines 6 - 7, The template injects raw HTML via {{{themeVars}}} and
{{{screenDivs}}}, enabling HTML/CSS injection if upstream content is untrusted;
fix by ensuring values passed into themeVars and screenDivs are
sanitized/allowlisted before rendering (escape or strip scripts/styles and
disallow dangerous attributes), and add defense-in-depth by emitting a strict
Content-Security-Policy header/meta (e.g., disallow inline scripts/styles,
restrict script/src/style-src to trusted origins) from the preview rendering
path so even if unsanitized markup appears it cannot execute.
Sources/Domain/Screenshots/ThemeDesign.swift-12-22 (1)

12-22: ⚠️ Potential issue | 🟠 Major

ThemeDesign is missing the required domain affordances contract.

This new Domain model does not conform to AffordanceProviding and does not expose an affordances field, which breaks the repo’s domain-model convention for agent navigation output.

As per coding guidelines, "All domain models must implement AffordanceProviding protocol with an affordances field containing ready-to-run CLI commands."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/ThemeDesign.swift` around lines 12 - 22,
ThemeDesign currently lacks the required AffordanceProviding conformance and an
affordances property; update the struct ThemeDesign to adopt the
AffordanceProviding protocol and add a public let affordances: [Affordance]
property (or computed var if preferred) populated with the domain-specific CLI
commands for this model, ensure init(palette:decorations:) is updated to accept
or synthesize affordances (or provide a secondary init) and implement any
required methods from AffordanceProviding so ThemeDesign (alongside
GalleryPalette and Decoration types) conforms to the repo convention.
Tests/ASCCommandTests/Commands/AppShots/AppShotsThemesTests.swift-198-217 (1)

198-217: ⚠️ Potential issue | 🟠 Major

Use a unique temp filename and guaranteed cleanup to avoid flaky parallel tests.

Using a fixed filename at Line 198 can collide across runs/suites, and cleanup at the end is skipped if the test exits early.

💡 Proposed reliability fix
-        let designPath = NSTemporaryDirectory() + "test-design.json"
-        let data = try JSONEncoder().encode(design)
-        try data.write(to: URL(fileURLWithPath: designPath))
+        let designPath = (NSTemporaryDirectory() as NSString)
+            .appendingPathComponent("test-design-\(UUID().uuidString).json")
+        let designURL = URL(fileURLWithPath: designPath)
+        let data = try JSONEncoder().encode(design)
+        try data.write(to: designURL)
+        defer { try? FileManager.default.removeItem(at: designURL) }
@@
-        try? FileManager.default.removeItem(atPath: designPath)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ASCCommandTests/Commands/AppShots/AppShotsThemesTests.swift` around
lines 198 - 217, The test uses a fixed designPath which can collide in parallel
runs and only deletes it at the end, so change the test that constructs
designPath (used when calling AppShotsThemesApplyDesign.parse and writing the
encoded design) to create a unique temporary file (e.g., by appending a UUID or
using a temp file API) and ensure deterministic cleanup by removing the file in
a defer block or a guaranteed teardown path; keep the rest of the setup
(MockTemplateRepository.getTemplate stub and cmd.execute call) unchanged but
reference the new unique designPath when invoking parse/execute.
Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift-174-178 (1)

174-178: ⚠️ Potential issue | 🟠 Major

Preserve the provided screenshot path for HTML preview.

Converting to lastPathComponent drops directory context and can break preview rendering for valid user paths.

Proposed fix
-            let shotForHTML = AppShot(screenshot: URL(fileURLWithPath: screenshot).lastPathComponent, type: .feature)
+            let shotForHTML = AppShot(screenshot: screenshot, type: .feature)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift` around lines
174 - 178, The code currently strips directory context by passing
URL(fileURLWithPath: screenshot).lastPathComponent into AppShot; change it to
preserve the full provided screenshot path when constructing the AppShot used
for HTML preview (i.e., pass the original screenshot path or its full file URL
string instead of lastPathComponent) so AppShot creation in this block
(shotForHTML / AppShot(...)) retains directory context and prevents broken
previews.
Sources/Domain/Screenshots/ThemedPage.swift-5-27 (1)

5-27: ⚠️ Potential issue | 🟠 Major

Align ThemedPage with the Domain model contract.

This new Domain model is missing required conformance/fields from repository standards: Codable, plus AffordanceProviding with an affordances field.

As per coding guidelines: Sources/Domain/**/*.swift: “All Domain layer models are public struct + Sendable + Equatable + Codable and “All domain models must implement AffordanceProviding protocol with an affordances field containing ready-to-run CLI commands.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/ThemedPage.swift` around lines 5 - 27, The
ThemedPage struct currently lacks required Domain contract conformance; update
ThemedPage to also conform to Codable and AffordanceProviding, add a public let
affordances: [Affordance] (or appropriate type from your domain) stored property
with a sensible default (e.g., empty array) and include it in the initializer,
and ensure conformance signatures include Codable and AffordanceProviding
alongside Sendable and Equatable; keep the existing html computed property and
initializer logic but add affordances to initializer parameters (with default)
so serialization and affordance provisioning work as expected.
Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift-14-16 (1)

14-16: ⚠️ Potential issue | 🟠 Major

Restore global renderer state after mutation.

This test mutates a global static and never resets it, which can contaminate later tests.

Proposed fix
-        if let frameData = try? Data(contentsOf: framePath) {
+        let previousPhoneFrameDataURL = GalleryHTMLRenderer.phoneFrameDataURL
+        defer { GalleryHTMLRenderer.phoneFrameDataURL = previousPhoneFrameDataURL }
+        if let frameData = try? Data(contentsOf: framePath) {
             GalleryHTMLRenderer.phoneFrameDataURL = "data:image/png;base64," + frameData.base64EncodedString()
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift` around
lines 14 - 16, The test mutates the global static
GalleryHTMLRenderer.phoneFrameDataURL and does not restore it, risking test
contamination; modify the test in GalleryPreviewOutputTests.swift to save the
original value of GalleryHTMLRenderer.phoneFrameDataURL before assigning the
base64 data and restore it after the test (use defer or a teardown) so the
global renderer state is reset regardless of test outcome.
Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift-34-45 (1)

34-45: ⚠️ Potential issue | 🟠 Major

Avoid writing preview artifacts into repository state during tests.

Writing under .asc/... in the project root makes the test non-hermetic and can pollute CI/local worktrees.

Proposed fix
-        let outputDir = projectRoot.appendingPathComponent(".asc/app-shots/gallery-previews")
+        let outputDir = FileManager.default.temporaryDirectory
+            .appendingPathComponent("gallery-previews-\(UUID().uuidString)", isDirectory: true)
+        defer { try? FileManager.default.removeItem(at: outputDir) }
         try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift` around
lines 34 - 45, The test currently writes preview files into the repo via the
outputDir variable
(projectRoot.appendingPathComponent(".asc/app-shots/gallery-previews")), which
can pollute CI/worktrees; change the test to create and use a temporary
directory instead (e.g., FileManager.default.temporaryDirectory with a unique
suffix or UUID) for outputDir, keep the same loop over galleries and use
gallery.previewHTML as before, and ensure the temp directory is created with
FileManager.default.createDirectory(...); also remove or update the printed path
so it doesn't reference repo-relative locations. This modifies how outputDir is
constructed and prevents writing into the repository state.
Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift-168-171 (1)

168-171: ⚠️ Potential issue | 🟠 Major

Fail fast when image preview is requested without a renderer.

Current control flow falls back to non-image output if preview == .image and renderer is nil, which is a wrong mode/result mismatch.

Proposed fix
-        if preview == .image, let renderer {
-            let html = template.apply(shot: shot, fillViewport: true)
-            return try await renderToImage(html: html, renderer: renderer)
-        }
+        if preview == .image {
+            guard let renderer else {
+                throw ValidationError("Preview format 'image' requires an HTML renderer.")
+            }
+            let html = template.apply(shot: shot, fillViewport: true)
+            return try await renderToImage(html: html, renderer: renderer)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift` around lines
168 - 171, The code currently falls through to non-image output when preview ==
.image but renderer is nil; change this to fail fast: before the existing
if-block that reads `if preview == .image, let renderer { ... }` add an explicit
guard that checks `if preview == .image && renderer == nil` and then
return/throw a clear error (e.g., throw a new PreviewRendererMissingError or
return a failure result) so the function does not silently produce the wrong
output; ensure the error type is defined and used where this function (the
AppShots template rendering path that calls `template.apply(shot:fillViewport:)`
and `renderToImage(html:renderer:)`) is allowed to propagate the failure.
Sources/Infrastructure/ScreenshotPlans/AggregateGalleryTemplateRepository.swift-25-27 (1)

25-27: ⚠️ Potential issue | 🟠 Major

Avoid full fan-out in getGallery when a match can be found early.

Current implementation always loads all providers via listGalleries(). This adds unnecessary latency/work for single-template lookup.

♻️ Suggested optimization
 public func getGallery(templateId: String) async throws -> Gallery? {
-    let all = try await listGalleries()
-    return all.first { $0.template?.id == templateId }
+    for provider in providers {
+        let galleries = try await provider.galleries()
+        if let match = galleries.first(where: { $0.template?.id == templateId }) {
+            return match
+        }
+    }
+    return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/Infrastructure/ScreenshotPlans/AggregateGalleryTemplateRepository.swift`
around lines 25 - 27, getGallery currently calls listGalleries() which fan-outs
to every provider; change it to short-circuit lookup by iterating providers and
asking each provider for the specific template (instead of fetching all
galleries). In AggregateGalleryTemplateRepository.getGallery(templateId:) call a
provider-level lookup (e.g., provider.getGallery(templateId:) or implement
provider.fetchGallery(templateId:)) for each provider and return immediately
when a non-nil Gallery is found; if you need concurrency, use a TaskGroup that
cancels remaining tasks when the first result is returned so work is not wasted.
Sources/Infrastructure/ScreenshotPlans/AggregateGalleryTemplateRepository.swift-12-14 (1)

12-14: ⚠️ Potential issue | 🟠 Major

Make provider registration idempotent on the shared repository.

register(provider:) blindly appends, so repeated bootstrap paths can produce duplicated providers and duplicated gallery rows.

🐛 Possible fix
 public final actor AggregateGalleryTemplateRepository: GalleryTemplateRepository {
     public static let shared = AggregateGalleryTemplateRepository()
 
     private var providers: [any GalleryTemplateProvider] = []
+    private var providerKeys = Set<String>()
@@
     public func register(provider: any GalleryTemplateProvider) {
+        let key = String(reflecting: type(of: provider))
+        guard providerKeys.insert(key).inserted else { return }
         providers.append(provider)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/Infrastructure/ScreenshotPlans/AggregateGalleryTemplateRepository.swift`
around lines 12 - 14, The register(provider:) method on
AggregateGalleryTemplateRepository currently appends providers blindly causing
duplicates; change it to be idempotent by checking the providers collection
before appending: in the register(provider: any GalleryTemplateProvider)
implementation, compare the incoming provider against existing entries in
providers (by identity or a unique provider identifier exposed by
GalleryTemplateProvider) and only append if not already present, ensuring
repeated bootstraps do not add duplicate providers or duplicated gallery rows.
Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift-9-23 (1)

9-23: ⚠️ Potential issue | 🟠 Major

This test is non-hermetic and has filesystem side effects

The test depends on repo-relative files and writes artifacts under .asc/..., making it environment-sensitive and CI-fragile. Prefer fixture resources in the test bundle and write outputs to a temp directory only when explicitly enabled (e.g., env flag).

Also applies to: 71-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift` around
lines 9 - 23, The test TemplatePreviewPageTests should stop depending on
repo-relative paths and writing into the repo; update the setup in
TemplatePreviewPageTests.swift to load the iPhone frame and templates.json from
bundled test fixtures (use the test bundle/Bundle.module or Bundle(for: ...))
instead of computing root via testFile/deletingLastPathComponent and use a
temporary directory (FileManager.default.temporaryDirectory or
NSTemporaryDirectory()) for any output artifacts, only writing there when an
explicit env flag (e.g., WRITE_TEST_ARTIFACTS) is set; replace usages of
framePath/jsonPath and direct Data(contentsOf:) with bundle resource lookup and
guard the write paths so GalleryHTMLRenderer.phoneFrameDataURL and any output
writes respect the temp directory + env flag.
Sources/Domain/Screenshots/Gallery/HTMLComposer.swift-32-34 (1)

32-34: ⚠️ Potential issue | 🟠 Major

Template-loading/render failures are silently swallowed

Returning "" or an empty MustacheLibrary on failure hides resource/template regressions and makes failures hard to diagnose. At minimum, emit diagnostics/assertions; ideally propagate a throwing error path.

🧭 Minimal diagnostic hardening
 public static func render(template name: String, with context: Any) -> String {
-    library.render(context, withTemplate: name) ?? ""
+    guard let output = library.render(context, withTemplate: name) else {
+        assertionFailure("Failed to render template '\(name)'")
+        return ""
+    }
+    return output
 }

Also applies to: 44-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/HTMLComposer.swift` around lines 32 - 34,
The render(template:with:) method (and the other render overloads using
library.render / MustacheLibrary) currently swallows failures by returning an
empty string; change these paths to surface errors by either making
render(template:with:) throwing and propagating the underlying error from
library.render (or converting a nil result into a descriptive thrown error), or
at minimum log/assert the failure including the template name and library error
information before returning; update the function signature(s)
(render(template:with:), any other render overloads that call library.render)
and callers accordingly so template-loading/render failures are not silently
ignored and include diagnostic details.
Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift-15-17 (1)

15-17: ⚠️ Potential issue | 🟠 Major

Global renderer state is mutated without restoration

GalleryHTMLRenderer.phoneFrameDataURL is set and never reset, so test order can change outcomes across the suite. Save previous value and restore via defer.

♻️ Suggested isolation fix
         if let frameData = try? Data(contentsOf: framePath) {
+            let previous = GalleryHTMLRenderer.phoneFrameDataURL
+            defer { GalleryHTMLRenderer.phoneFrameDataURL = previous }
             GalleryHTMLRenderer.phoneFrameDataURL = "data:image/png;base64," + frameData.base64EncodedString()
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift` around
lines 15 - 17, The test mutates global state by setting
GalleryHTMLRenderer.phoneFrameDataURL without restoring it; capture the current
value into a local (e.g., let previous = GalleryHTMLRenderer.phoneFrameDataURL)
before assigning the test value, then restore it in a defer block (defer {
GalleryHTMLRenderer.phoneFrameDataURL = previous }) so the original global is
always returned after the test completes; apply this change around the code that
reads framePath and assigns the base64 string.
Sources/ASCCommand/Commands/Web/Controllers/AppShotsController.swift-54-64 (1)

54-64: ⚠️ Potential issue | 🟠 Major

Make screenshot staging fail-fast and self-cleaning.

These loops silently skip invalid base64 items and never delete the temp PNGs they do write. That means bad payloads can produce partial/misaligned output, and repeated requests will leak disk space on the server. Decode the whole batch eagerly, return 400 on the first invalid item, and defer cleanup of every staged file after rendering.

Also applies to: 117-127

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ASCCommand/Commands/Web/Controllers/AppShotsController.swift` around
lines 54 - 64, The current loop over screenshotsB64 silently skips invalid
base64 and never removes temporary files; change it to eagerly validate and
decode every item (iterate screenshotsB64, attempt Data(base64Encoded:) for each
and if any decode fails immediately return a 400 response), only write files if
all decodes succeed, collect file paths into paths and dataURLs as before, and
ensure you schedule cleanup with defer to remove every staged temp PNG after
rendering (use FileManager.removeItem on each path). Apply the same eager
validation, fail-fast 400 behavior, and deferred cleanup to the second staging
block referenced around lines 117-127 as well.
Sources/ASCCommand/Commands/AppShots/AppShotsThemes.swift-103-109 (1)

103-109: ⚠️ Potential issue | 🟠 Major

Route design output through OutputFormatter.

This hardcodes pretty JSON and bypasses the standard formatter, so --output/--pretty are ignored and the new command never injects CLI affordances into its response.

As per coding guidelines, "All CLI commands must include an affordances field in responses with ready-to-run CLI commands for agent navigation" and "Use OutputFormatter.formatAgentItems() to merge affordances into JSON output".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ASCCommand/Commands/AppShots/AppShotsThemes.swift` around lines 103 -
109, The execute(themeRepo: any ThemeRepository) method currently encodes the
design with a hardcoded JSONEncoder; replace that with the CLI OutputFormatter
so --output/--pretty flags are respected and CLI affordances are injected: call
ThemeRepository.design(id:), build the affordances object required by our CLI,
then pass the design and affordances into OutputFormatter.formatAgentItems(...)
(or the equivalent OutputFormatter API used elsewhere) and return its result
string instead of manually encoding with JSONEncoder; update references in this
function (execute and the design result handling) to use
OutputFormatter.formatAgentItems and ensure the returned JSON includes the
affordances field.
Sources/Domain/Screenshots/AppShotTemplate.swift-74-78 (1)

74-78: ⚠️ Potential issue | 🟠 Major

Use full-value equality here.

supportedSizes and description are omitted, so two distinct templates can compare equal. That will break any diffing, caching, or tests that rely on AppShotTemplate being a real value type.

♻️ Proposed fix
-extension AppShotTemplate: Equatable {
-    public static func == (lhs: AppShotTemplate, rhs: AppShotTemplate) -> Bool {
-        lhs.id == rhs.id && lhs.name == rhs.name && lhs.category == rhs.category
-            && lhs.screenLayout == rhs.screenLayout && lhs.palette == rhs.palette
-    }
-}
+extension AppShotTemplate: Equatable {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/AppShotTemplate.swift` around lines 74 - 78, The
custom Equatable implementation for AppShotTemplate omits supportedSizes and
description, causing different instances to compare equal; update the ==
operator in the AppShotTemplate extension to compare all stored properties
(e.g., include lhs.supportedSizes == rhs.supportedSizes and lhs.description ==
rhs.description) so equality is full-value, or remove the custom implementation
so the compiler can synthesize Equatable if all properties are Equatable.
Sources/Domain/Screenshots/Gallery/Gallery.swift-125-131 (1)

125-131: ⚠️ Potential issue | 🟠 Major

previewHTML will go stale after the first render.

The cache key is only gallery-\(id), but template, palette, and every AppShot’s content can still change afterward. Once this property is read once, later edits will keep returning the old preview.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/Gallery.swift` around lines 125 - 131, The
previewHTML property currently uses a static cache key ("gallery-\(id)") so it
becomes stale; change the key passed to GalleryHTMLRenderer.cachedPreview to
include the gallery's mutable state (for example include template, palette and
each AppShot's content or a combined hash/timestamp) so that
GalleryHTMLRenderer.cachedPreview("gallery-\(id)-\(stateHash)") will invalidate
when template, palette, or any AppShot changes; update the key construction
where previewHTML calls GalleryHTMLRenderer.cachedPreview and keep calling
GalleryHTMLRenderer.renderPreviewPage(self) for generation.
Sources/Domain/Screenshots/Gallery/Gallery.swift-16-23 (1)

16-23: ⚠️ Potential issue | 🟠 Major

Gallery has the same mutable @unchecked Sendable problem.

This is another public reference type with mutable state inside Domain, so cross-task safety now depends on caller discipline instead of the type system. That departs from the repository’s Domain-model contract.

As per coding guidelines, All Domain layer models are public struct + Sendable + Equatable + Codable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/Gallery.swift` around lines 16 - 23,
Gallery is a public reference type with mutable state and uses `@unchecked`
Sendable; change it to a value type that satisfies the Domain contract: convert
the public final class Gallery to a public struct Gallery, remove `@unchecked`
Sendable, and add Sendable, Equatable and Codable conformances; keep id and
appName as immutable lets, keep appShots/template/palette as stored properties
(use private(set) where mutability should be restricted), add/update an
initializer that sets those properties, and ensure referenced types (AppShot,
GalleryTemplate, GalleryPalette) conform to Sendable/Equatable/Codable if
necessary and update any call sites that relied on reference semantics (e.g.,
identity or inout expectations).
Sources/Domain/Screenshots/Gallery/AppShot.swift-14-24 (1)

14-24: ⚠️ Potential issue | 🟠 Major

AppShot breaks the Domain value-model contract.

Making this a mutable reference type with @unchecked Sendable sidesteps the thread-safety guarantee the Domain layer is supposed to provide. It also leaves the model without parent context, so its affordances cannot be reliably emitted as ready-to-run commands.

As per coding guidelines, All Domain layer models are public struct + Sendable + Equatable + Codable and Every Domain model must carry its parent ID (e.g. AppStoreVersion.appId, AppScreenshot.setId).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/AppShot.swift` around lines 14 - 24,
Replace the mutable reference class AppShot with an immutable value type: change
public final class AppShot: `@unchecked` Sendable, Identifiable to public struct
AppShot: Sendable, Identifiable, Equatable, Codable and make all fields
immutable (use let for id, screenshots, type, tagline, headline, body, badges,
trustMarks) so the model is thread-safe; remove `@unchecked` Sendable and ensure
synthesized Equatable/Codable work. Add an explicit parent identifier property
(e.g., let appId: String or let setId: String) to carry the parent context
required by Domain affordances, and update any initializer(s) and uses of
AppShot to pass that parent ID. Ensure ScreenType remains compatible with
Codable/Sendable or adjust accordingly.
Sources/Domain/Screenshots/Gallery/ScreenLayout.swift-223-228 (1)

223-228: ⚠️ Potential issue | 🟠 Major

Fail fast on malformed DecorationShape objects.

If the object form decodes but does not contain label, this silently becomes .gem. That hides bad ThemeDesign JSON and renders the wrong decoration instead of surfacing the schema error.

Suggested fix
         let dict = try container.decode([String: String].self)
-        if let text = dict["label"] {
-            self = .label(text)
-        } else {
-            self = .gem
-        }
+        guard let text = dict["label"] else {
+            throw DecodingError.dataCorruptedError(
+                in: container,
+                debugDescription: "Expected a decoration shape string or an object with a `label` field."
+            )
+        }
+        self = .label(text)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/ScreenLayout.swift` around lines 223 -
228, The decoding currently treats a decoded dictionary without a "label" key as
.gem, hiding malformed ThemeDesign JSON; update DecorationShape's Decodable init
(the init(from:) that decodes [String: String]) to validate the dict and throw a
DecodingError (e.g., .dataCorrupted or .keyNotFound with a clear context) when
"label" is missing instead of defaulting to .gem, so invalid input surfaces as a
decode failure; include the key name "label" and the decoded dictionary in the
error context to aid debugging.
Sources/Domain/Screenshots/Gallery/AppShot.swift-64-100 (1)

64-100: ⚠️ Potential issue | 🟠 Major

This Codable implementation does not round-trip cleanly.

id is never encoded or decoded, so every deserialized AppShot gets a fresh UUID. Also, if screenshots is empty, Line 93 serializes screenshot as "", which turns “no screenshots” into bogus data.

Suggested fix
 extension AppShot: Codable {
     private enum CodingKeys: String, CodingKey {
-        case screenshot, screenshots, type, tagline, headline, body, badges, trustMarks
+        case id, screenshot, screenshots, type, tagline, headline, body, badges, trustMarks
     }

     public convenience init(from decoder: any Decoder) throws {
         let c = try decoder.container(keyedBy: CodingKeys.self)
+        let id = try c.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
         // Support both "screenshot" (single) and "screenshots" (array)
         let shots: [String]
         if let arr = try? c.decode([String].self, forKey: .screenshots) {
             shots = arr
         } else if let single = try? c.decode(String.self, forKey: .screenshot) {
             shots = [single]
         } else {
             shots = []
         }
         let type = try c.decodeIfPresent(ScreenType.self, forKey: .type) ?? .feature
-        self.init(screenshots: shots, type: type)
+        self.init(id: id, screenshots: shots, type: type)
         self.tagline = try c.decodeIfPresent(String.self, forKey: .tagline)
         self.headline = try c.decodeIfPresent(String.self, forKey: .headline)
         self.body = try c.decodeIfPresent(String.self, forKey: .body)
         self.badges = try c.decodeIfPresent([String].self, forKey: .badges) ?? []
         self.trustMarks = try c.decodeIfPresent([String].self, forKey: .trustMarks)
     }

     public func encode(to encoder: any Encoder) throws {
         var c = encoder.container(keyedBy: CodingKeys.self)
+        try c.encode(id, forKey: .id)
         if screenshots.count > 1 {
             try c.encode(screenshots, forKey: .screenshots)
-        } else {
-            try c.encode(screenshot, forKey: .screenshot)
+        } else if let first = screenshots.first {
+            try c.encode(first, forKey: .screenshot)
+        } else {
+            try c.encode([], forKey: .screenshots)
         }
         try c.encode(type, forKey: .type)
         try c.encodeIfPresent(tagline, forKey: .tagline)
         try c.encodeIfPresent(headline, forKey: .headline)
         try c.encodeIfPresent(body, forKey: .body)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/AppShot.swift` around lines 64 - 100, The
Codable implementation for AppShot should preserve the id and avoid emitting a
bogus empty "screenshot" string: add "id" to CodingKeys and decode/encode the id
in init(from:) and encode(to:), so deserialized AppShot keeps its original UUID;
update init(from:) to treat an empty decoded single screenshot string as no
screenshots (i.e., if decoding .screenshot yields "" then set shots = []), and
update encode(to:) to only emit .screenshot when screenshots.count == 1 and the
single value is non-empty (omit both .screenshot and .screenshots when
screenshots.isEmpty) so no empty-string placeholder is written.
Sources/Domain/Screenshots/Gallery/Gallery.swift-145-168 (1)

145-168: ⚠️ Potential issue | 🟠 Major

Gallery persistence drops identity and stores a derived cache.

id is not serialized, so a decoded gallery becomes a different entity. At the same time, previewHTML is encoded even though it is derived and ignored on decode, which makes the payload stale and non-round-trippable.

Suggested fix
 extension Gallery: Codable {
     private enum CodingKeys: String, CodingKey {
-        case appName, appShots, template, palette, previewHTML
+        case id, appName, appShots, template, palette
     }

     public convenience init(from decoder: any Decoder) throws {
         let c = try decoder.container(keyedBy: CodingKeys.self)
+        let id = try c.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
         let appName = try c.decode(String.self, forKey: .appName)
         // Decode appShots directly — they carry their own screenshot/type/content
         let shots = try c.decode([AppShot].self, forKey: .appShots)
-        self.init(appName: appName, screenshots: [])
+        self.init(id: id, appName: appName, screenshots: [])
         self.appShots = shots
         self.template = try c.decodeIfPresent(GalleryTemplate.self, forKey: .template)
         self.palette = try c.decodeIfPresent(GalleryPalette.self, forKey: .palette)
     }

     public func encode(to encoder: any Encoder) throws {
         var c = encoder.container(keyedBy: CodingKeys.self)
+        try c.encode(id, forKey: .id)
         try c.encode(appName, forKey: .appName)
         try c.encode(appShots, forKey: .appShots)
         try c.encodeIfPresent(template, forKey: .template)
         try c.encodeIfPresent(palette, forKey: .palette)
-        try c.encode(previewHTML, forKey: .previewHTML)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/Gallery.swift` around lines 145 - 168, The
Gallery Codable implementation currently omits the primary identity and encodes
a derived field; update the CodingKeys to include id, decode id in init(from:)
and encode id in encode(to:) so decoded Gallery preserves identity (referencing
Gallery, CodingKeys, init(from:), encode(to:)), and stop encoding previewHTML
(remove previewHTML from encode or mark it as transient) so derived previewHTML
is not persisted and the payload is round-trippable.
🟡 Minor comments (4)
docs/features/app-shots.md-32-32 (1)

32-32: ⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks (MD040).

Several changed fences are missing explicit languages and will keep markdownlint warnings active.

Suggested fixes
-```
+```bash
 asc app-shots templates list
 ...
-```
+```

-```
+```text
 ASCCommand                       Domain                                  Infrastructure
 ...
-```
+```

-```
+```text
 1cqi = 1% of the container's inline size
-```
+```

Also applies to: 165-165, 221-221, 236-236, 258-258, 424-424

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/app-shots.md` at line 32, The markdown file
docs/features/app-shots.md contains several fenced code blocks missing language
identifiers (MD040); update each affected triple-backtick fence to include an
appropriate language tag (e.g., use ```bash for the command block containing
"asc app-shots templates list", and ```text for the ASCII table block starting
with "ASCCommand                       Domain                                 
Infrastructure" and for the explanatory line "1cqi = 1% of the container's
inline size") so all code fences have explicit languages and the markdownlint
warnings are resolved.
docs/features/app-shots-themes.md-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

Use one REST route prefix consistently.

This example calls POST /themes/design, but later in the same doc the endpoints are documented under /app-shots/themes/.... One of these is stale, and the current mismatch makes the copy-paste flow unreliable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/app-shots-themes.md` around lines 38 - 40, The doc uses two
different route prefixes for the same action: `POST /themes/design` vs the
`/app-shots/themes/...` family; pick one prefix and update the example to match
the rest of the document (e.g., change `POST /themes/design {themeId}` to `POST
/app-shots/themes/design {themeId}`) and ensure any references to the
deterministic re-render call (`ThemeDesignApplier.apply(design, shot, layout)`)
remain consistent with the chosen endpoint naming.
docs/features/app-shots-themes.md-347-350 (1)

347-350: ⚠️ Potential issue | 🟡 Minor

buildContext() is described as returning HTML, not a prompt.

The method description says buildContext() “returns full HTML”, but the rest of the page describes it as the prompt builder for the AI provider. That wording will mislead plugin implementers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features/app-shots-themes.md` around lines 347 - 350, Update the docs
entry for buildContext() to correctly describe it as producing the theme prompt
for the AI provider rather than returning full HTML: change the
Returns/Description text so buildContext() is described as returning a String
prompt (the theme prompt used by the AI provider) and ensure it contrasts with
buildDesignContext() which returns a ThemeDesign JSON prompt; reference the
buildContext() symbol in the edit so implementers understand it produces the
prompt content rather than rendered HTML.
CHANGELOG.md-10-30 (1)

10-30: ⚠️ Potential issue | 🟡 Minor

Add the missing Fixed subsection under [Unreleased].

This entry only has Added and Changed, but the repository changelog contract requires Added, Changed, and Fixed under [Unreleased].

As per coding guidelines, CHANGELOG.md must add changelog entries under [Unreleased] using Keep a Changelog format with categories: Added, Changed, Fixed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 10 - 30, The [Unreleased] changelog block is
missing the required "Fixed" subsection; update CHANGELOG.md by adding a "###
Fixed" header under the [Unreleased] section (beside the existing "Added" and
"Changed" headings) and include at least one bullet (can be a placeholder like
"- <describe fix here>" or a real fix) so the file conforms to the Keep a
Changelog contract; ensure the new "### Fixed" sits alongside the existing
entries (e.g., after "Changed") and follows the same formatting style as the
other subsections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0c90b74-8d6b-4852-b40e-1a5007832d9c

📥 Commits

Reviewing files that changed from the base of the PR and between f529baa and 3a25077.

📒 Files selected for processing (77)
  • CHANGELOG.md
  • Package.resolved
  • Package.swift
  • Sources/ASCCommand/ClientProvider.swift
  • Sources/ASCCommand/Commands/AppShots/AppShotsCommand.swift
  • Sources/ASCCommand/Commands/AppShots/AppShotsExport.swift
  • Sources/ASCCommand/Commands/AppShots/AppShotsGalleryTemplates.swift
  • Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift
  • Sources/ASCCommand/Commands/AppShots/AppShotsThemes.swift
  • Sources/ASCCommand/Commands/Web/Controllers/AppShotsController.swift
  • Sources/ASCCommand/Commands/Web/RESTRoutes.swift
  • Sources/Domain/ScreenshotPlans/LayoutMode.swift
  • Sources/Domain/ScreenshotPlans/ScreenDesign.swift
  • Sources/Domain/ScreenshotPlans/ScreenshotTemplate.swift
  • Sources/Domain/ScreenshotPlans/TemplateContent.swift
  • Sources/Domain/ScreenshotPlans/TemplateHTMLRenderer.swift
  • Sources/Domain/ScreenshotPlans/ThemedPage.swift
  • Sources/Domain/Screenshots/AppShotTemplate.swift
  • Sources/Domain/Screenshots/AppShotsConfig.swift
  • Sources/Domain/Screenshots/AppShotsConfigStorage.swift
  • Sources/Domain/Screenshots/Gallery/AppShot.swift
  • Sources/Domain/Screenshots/Gallery/Gallery.swift
  • Sources/Domain/Screenshots/Gallery/GalleryHTMLRenderer.swift
  • Sources/Domain/Screenshots/Gallery/GalleryPalette.swift
  • Sources/Domain/Screenshots/Gallery/GalleryTemplate.swift
  • Sources/Domain/Screenshots/Gallery/GalleryTemplateRepository.swift
  • Sources/Domain/Screenshots/Gallery/HTMLComposer.swift
  • Sources/Domain/Screenshots/Gallery/Resources/keyframes.mustache
  • Sources/Domain/Screenshots/Gallery/Resources/page-wrapper.mustache
  • Sources/Domain/Screenshots/Gallery/Resources/preview-page.mustache
  • Sources/Domain/Screenshots/Gallery/Resources/preview-screen.mustache
  • Sources/Domain/Screenshots/Gallery/Resources/screen.mustache
  • Sources/Domain/Screenshots/Gallery/Resources/theme-vars.mustache
  • Sources/Domain/Screenshots/Gallery/Resources/wireframe.mustache
  • Sources/Domain/Screenshots/Gallery/ScreenLayout.swift
  • Sources/Domain/Screenshots/HTMLRenderer.swift
  • Sources/Domain/Screenshots/ScreenTheme.swift
  • Sources/Domain/Screenshots/ScreenshotPlans+RESTRoutes.swift
  • Sources/Domain/Screenshots/TemplateRepository.swift
  • Sources/Domain/Screenshots/ThemeDesign.swift
  • Sources/Domain/Screenshots/ThemeDesignApplier.swift
  • Sources/Domain/Screenshots/ThemedPage.swift
  • Sources/Infrastructure/ScreenshotPlans/AggregateGalleryTemplateRepository.swift
  • Sources/Infrastructure/ScreenshotPlans/AggregateTemplateRepository.swift
  • Sources/Infrastructure/ScreenshotPlans/AggregateThemeRepository.swift
  • Tests/ASCCommandTests/Commands/AppShots/AppShotsGalleryTemplatesTests.swift
  • Tests/ASCCommandTests/Commands/AppShots/AppShotsTemplatesTests.swift
  • Tests/ASCCommandTests/Commands/AppShots/AppShotsThemesTests.swift
  • Tests/ASCCommandTests/Commands/Web/AppShotsControllerTests.swift
  • Tests/DomainTests/ScreenshotPlans/ScreenDesignTests.swift
  • Tests/DomainTests/ScreenshotPlans/TemplateApplyTests.swift
  • Tests/DomainTests/Screenshots/AppShotTemplateTests.swift
  • Tests/DomainTests/Screenshots/AppShotsConfigTests.swift
  • Tests/DomainTests/Screenshots/Gallery/AppShotTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryApplyTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryCodableTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryComposeTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryHTMLRendererTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryPreviewTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryTemplateRepositoryTests.swift
  • Tests/DomainTests/Screenshots/Gallery/GalleryTests.swift
  • Tests/DomainTests/Screenshots/Gallery/HTMLComposerTests.swift
  • Tests/DomainTests/Screenshots/Gallery/ScreenLayoutTests.swift
  • Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift
  • Tests/DomainTests/Screenshots/Gallery/TextSlotPreviewTests.swift
  • Tests/DomainTests/Screenshots/ScreenThemeTests.swift
  • Tests/DomainTests/Screenshots/TemplateApplyTests.swift
  • Tests/DomainTests/Screenshots/TemplateRenderTests.swift
  • Tests/DomainTests/Screenshots/ThemeDesignApplierTests.swift
  • Tests/DomainTests/Screenshots/ThemeDesignTests.swift
  • Tests/DomainTests/Shared/PresentableTests.swift
  • Tests/DomainTests/TestHelpers/MockRepositoryFactory.swift
  • Tests/InfrastructureTests/ScreenshotPlans/AggregateTemplateRepositoryTests.swift
  • apps/asc-web/command-center/css/layout.css
  • docs/features/app-shots-themes.md
  • docs/features/app-shots.md
💤 Files with no reviewable changes (8)
  • Sources/Domain/ScreenshotPlans/LayoutMode.swift
  • Tests/DomainTests/ScreenshotPlans/TemplateApplyTests.swift
  • Sources/Domain/ScreenshotPlans/ThemedPage.swift
  • Sources/Domain/ScreenshotPlans/TemplateContent.swift
  • Tests/DomainTests/ScreenshotPlans/ScreenDesignTests.swift
  • Sources/Domain/ScreenshotPlans/ScreenDesign.swift
  • Sources/Domain/ScreenshotPlans/TemplateHTMLRenderer.swift
  • Sources/Domain/ScreenshotPlans/ScreenshotTemplate.swift

Comment on lines +16 to +29
nonisolated(unsafe) private static var _library: MustacheLibrary?

/// The template library. Lazily loads from bundle Resources on first access.
public static var library: MustacheLibrary {
if let lib = _library { return lib }
let lib = loadLibrary()
_library = lib
return lib
}

/// Replace the library (e.g. for plugins providing custom templates).
public static func setLibrary(_ lib: MustacheLibrary) {
_library = lib
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Shared mutable state declarations/usages:"
rg -n --type swift 'private static var _library|public static var library|setLibrary\('

echo
echo "Call sites of HTMLComposer accessors:"
rg -n --type swift 'HTMLComposer\.(library|setLibrary|render)\('

Repository: tddworks/asc-cli

Length of output: 4811


🏁 Script executed:

cat -n Sources/Domain/Screenshots/Gallery/HTMLComposer.swift

Repository: tddworks/asc-cli

Length of output: 3043


🏁 Script executed:

# Check the GalleryHTMLRenderer context to understand if these are called concurrently
head -80 Sources/Domain/Screenshots/Gallery/GalleryHTMLRenderer.swift | cat -n

Repository: tddworks/asc-cli

Length of output: 3227


🏁 Script executed:

# Check if GalleryHTMLRenderer methods are async or if they call HTMLComposer from concurrent contexts
rg -n --type swift -A 3 'public static func.*render' Sources/Domain/Screenshots/Gallery/GalleryHTMLRenderer.swift

Repository: tddworks/asc-cli

Length of output: 505


🏁 Script executed:

# Check for async/await, Task, DispatchQueue usage in GalleryHTMLRenderer
rg -n --type swift 'async|await|Task|DispatchQueue|@MainActor' Sources/Domain/Screenshots/Gallery/GalleryHTMLRenderer.swift

Repository: tddworks/asc-cli

Length of output: 42


🏁 Script executed:

# Check where GalleryHTMLRenderer methods are called from
rg -n --type swift 'GalleryHTMLRenderer\.(renderScreen|renderGallery|renderPreviewPage)' Sources/ Tests/

Repository: tddworks/asc-cli

Length of output: 2098


🏁 Script executed:

# Check if there are any async contexts in the call chain
rg -n --type swift 'async func|func.*async' Sources/Domain/Screenshots/ | head -20

Repository: tddworks/asc-cli

Length of output: 1886


🏁 Script executed:

# Check AppShotsController context to see if these are handling concurrent requests
sed -n '140,210p' Sources/ASCCommand/Commands/Web/Controllers/AppShotsController.swift | cat -n

Repository: tddworks/asc-cli

Length of output: 4485


🏁 Script executed:

# Check if there's any actor isolation or concurrency safety in the codebase architecture
rg -n --type swift '@Actor|nonisolated|Sendable' Sources/Domain/ | head -15

Repository: tddworks/asc-cli

Length of output: 1879


Unsynchronized mutable static state introduces a race in template library initialization

The _library property is read and written from concurrent web request handlers without synchronization. Requests arriving concurrently can race on both the lazy initialization (line 20–23) and setLibrary() (line 28), producing nondeterministic behavior. This is particularly critical since AppShotsController handles HTTP requests asynchronously, and each can trigger the getter. The nonisolated(unsafe) annotation explicitly opts out of actor isolation without providing thread safety — unlike PreviewCache in the same module, which correctly uses NSLock().

Additionally, rendering failures are silently swallowed: missing templates return empty strings (line 33), parse errors return the template unparsed (line 38), and resource loading errors return empty libraries (lines 46, 51).

🔒 Suggested fix
 public enum HTMLComposer {
-    nonisolated(unsafe) private static var _library: MustacheLibrary?
+    private static var _library: MustacheLibrary?
+    private static let libraryLock = NSLock()

     public static var library: MustacheLibrary {
+        libraryLock.lock()
+        defer { libraryLock.unlock() }
         if let lib = _library { return lib }
         let lib = loadLibrary()
         _library = lib
         return lib
     }

     public static func setLibrary(_ lib: MustacheLibrary) {
+        libraryLock.lock()
+        defer { libraryLock.unlock() }
         _library = lib
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nonisolated(unsafe) private static var _library: MustacheLibrary?
/// The template library. Lazily loads from bundle Resources on first access.
public static var library: MustacheLibrary {
if let lib = _library { return lib }
let lib = loadLibrary()
_library = lib
return lib
}
/// Replace the library (e.g. for plugins providing custom templates).
public static func setLibrary(_ lib: MustacheLibrary) {
_library = lib
}
private static var _library: MustacheLibrary?
private static let libraryLock = NSLock()
/// The template library. Lazily loads from bundle Resources on first access.
public static var library: MustacheLibrary {
libraryLock.lock()
defer { libraryLock.unlock() }
if let lib = _library { return lib }
let lib = loadLibrary()
_library = lib
return lib
}
/// Replace the library (e.g. for plugins providing custom templates).
public static func setLibrary(_ lib: MustacheLibrary) {
libraryLock.lock()
defer { libraryLock.unlock() }
_library = lib
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/HTMLComposer.swift` around lines 16 - 29,
The _library static is unsafely accessed concurrently: protect reads/writes to
nonisolated(unsafe) private static var _library by introducing a synchronization
primitive (e.g. an NSLock or DispatchQueue) used inside the public static var
library getter and public static func setLibrary(_:) so lazy initialization
(library getter calling loadLibrary()) and replacement via setLibrary(_) are
atomic and race-free; also update loadLibrary() and the template rendering paths
it uses to surface or log errors instead of swallowing them (do not return empty
strings or silent fallbacks)—have loadLibrary propagate or log resource/parse
errors and ensure the library getter logs/propagates load failures so callers
(e.g. AppShotsController) can handle them deterministically.

@@ -0,0 +1 @@
{{{themeVars}}}<div data-theme="{{theme}}" style="background:{{background}};position:relative;overflow:hidden;container-type:inline-size;width:100%;height:100%">{{#tagline}}<div style="position:absolute;top:{{top}}%;left:{{pad}}%;right:{{pad}}%;z-index:4;font-weight:{{weight}};font-size:{{fontSize}}cqi;color:{{color}};letter-spacing:0.1em;text-transform:uppercase;text-align:{{align}};white-space:pre-line">{{{content}}}</div>{{/tagline}}{{#headline}}<div style="position:absolute;top:{{top}}%;left:{{pad}}%;right:{{pad}}%;z-index:4;font-weight:{{weight}};font-size:{{fontSize}}cqi;color:{{color}};line-height:0.92;letter-spacing:-0.03em;text-align:{{align}};white-space:pre-line">{{{content}}}</div>{{/headline}}{{#subheading}}<div style="position:absolute;top:{{top}}%;left:{{pad}}%;right:{{padRight}}%;z-index:4;font-weight:{{weight}};font-size:{{fontSize}}cqi;color:var(--body-color);line-height:1.4;text-align:{{align}}">{{{content}}}</div>{{/subheading}}{{#trustMarks}}<div style="position:absolute;top:{{top}}%;left:{{pad}}%;z-index:4;display:flex;gap:4px;flex-wrap:wrap">{{#items}}<span style="background:var(--badge-bg);border-radius:5px;padding:0.3cqi 0.8cqi;font-size:{{fontSize}}cqi;font-weight:700;color:var(--trust-color);letter-spacing:0.04em">{{text}}</span>{{/items}}</div>{{/trustMarks}}{{#badges}}<div style="position:absolute;left:{{left}}%;top:{{top}}%;z-index:5;background:var(--badge-bg);border:1px solid var(--badge-border);border-radius:100px;padding:0.3cqi 0.8cqi;font-size:{{fontSize}}cqi;font-weight:700;color:var(--badge-color);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);white-space:nowrap">{{text}}</div>{{/badges}}{{#devices}}{{#hasScreenshot}}<div style="position:absolute;left:{{left}}%;top:{{top}}%;width:{{width}}%;z-index:2"><img src="{{screenshot}}" style="width:100%;display:block" alt=""></div>{{/hasScreenshot}}{{#hasWireframe}}<div style="position:absolute;left:{{left}}%;top:{{top}}%;width:{{width}}%;z-index:2">{{#hasPhoneFrame}}<div style="aspect-ratio:1470/3000;position:relative;filter:drop-shadow(0 4px 20px rgba(0,0,0,var(--shadow-opacity)))">{{{wireframeHTML}}}<img src="{{phoneFrameURL}}" style="position:absolute;inset:0;width:100%;height:100%;z-index:2;pointer-events:none" alt=""></div>{{/hasPhoneFrame}}{{#noPhoneFrame}}<div style="aspect-ratio:1470/3000;position:relative;filter:drop-shadow(0 4px 20px rgba(0,0,0,var(--shadow-opacity)));background:var(--frame-bg);border-radius:12%/5.5%;border:1.5px solid var(--frame-border);overflow:hidden">{{{wireframeHTML}}}</div>{{/noPhoneFrame}}</div>{{/hasWireframe}}{{/devices}}{{#decorations}}<div style="position:absolute;left:{{left}}%;top:{{top}}%;z-index:3;font-size:{{fontSize}}cqi;opacity:{{opacity}};background:{{background}};color:{{#useDefaultColor}}var(--deco-color){{/useDefaultColor}}{{color}};border-radius:{{borderRadius}};padding:0.3cqi 0.8cqi;pointer-events:none;white-space:nowrap;{{animStyle}}">{{{content}}}</div>{{/decorations}}{{#hasAnimations}}{{{keyframesHTML}}}{{/hasAnimations}}</div> No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In swift-mustache, does {{{name}}}render unescaped HTML while{{name}} HTML-escapes output?

💡 Result:

Yes. In swift-mustache (Hummingbird’s Swift Mustache implementation), {{name}} renders HTML-escaped output by default, while {{{name}}} renders the value unescaped (raw HTML). [1][2]


🏁 Script executed:

#!/bin/bash

# Find references to screen.mustache and understand context
echo "=== Finding references to screen.mustache ==="
rg "screen\.mustache" --type-list | head -20
rg "screen\.mustache" -l

echo -e "\n=== Exploring directory structure ==="
find Sources/Domain/Screenshots -type f -name "*.swift" -o -name "*.ts" -o -name "*.js" | head -20

Repository: tddworks/asc-cli

Length of output: 1521


🏁 Script executed:

#!/bin/bash

# Look for how content/tagline/headline are populated and if they're sanitized
echo "=== Searching for content field assignments ==="
rg "content" Sources/Domain/Screenshots -t swift -A 2 -B 2 | head -80

echo -e "\n=== Searching for sanitization patterns ==="
rg "sanitize|escape|html" Sources/Domain/Screenshots -t swift | head -20

Repository: tddworks/asc-cli

Length of output: 8668


🏁 Script executed:

#!/bin/bash

# Look for the structure that builds the template context
echo "=== Searching for wireframeHTML, keyframesHTML (trusted generated HTML) ==="
rg "wireframeHTML|keyframesHTML" Sources/Domain/Screenshots -t swift -A 2 -B 2 | head -50

Repository: tddworks/asc-cli

Length of output: 1295


Escape user-provided text before rendering to prevent XSS injection.

The template injects user content from AppShot (tagline, headline, body) directly into the page using unescaped {{{content}}} variables. A user providing <img src=x onerror="alert()"> as a headline would execute arbitrary JavaScript. This affects tagline, headline, subheading, and decoration text slots.

The codebase intentionally replaces newlines with <br> tags before passing content to the template. To fix this securely, HTML-escape the user text first, then insert the intentional <br> tags (or use a dedicated template variable for safe HTML fragments), then render with {{{safecontent}}}. Alternatively, apply escaping in the template itself using {{content}} while handling line breaks separately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Domain/Screenshots/Gallery/Resources/screen.mustache` at line 1, The
template renders unescaped user text via {{{content}}} for tagline, headline,
subheading, and decorations causing XSS; fix by HTML-escaping user-provided
strings on the server (escape &, <, >, ", ') then re-insert the intended <br>
line breaks (or build a separate safeHtml fragment) and expose that escaped
fragment as e.g. safeContent (or safeTagline/safeHeadline/etc.), then update the
mustache usage to render the pre-sanitized HTML fragment (e.g.
{{{safeContent}}}) or switch to the escaped variable {{content}} and handle
breaks separately; update all occurrences where {{{content}}} is used (tagline,
headline, subheading, decorations) so only escaped/sanitized content is
injected.

@hanrw hanrw changed the title Support Gallery templates feat(gallery): Gallery templates, ThemeDesign, and Mustache rendering Apr 9, 2026
- Simplify GalleryPreviewOutputTests with inline sample data
- Add tests for empty and dark-themed gallery previews
- Refactor TemplatePreviewPageTests to use hardcoded templates
- Cover multi-device layouts and gradient backgrounds in previews
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift (1)

31-33: Prefer user-visible assertions over template internals

These checks are tightly coupled to implementation tokens (container-type, raw hex snippets, <!DOCTYPE html>, 9:41). Consider asserting what a user would perceive in preview output (e.g., expected headline content, presence of preview sections/devices via stable semantic markers) instead of CSS/HTML internals.

♻️ Suggested direction
- `#expect`(html.contains("<!DOCTYPE html>"))
- `#expect`(html.contains("container-type"))
+ `#expect`(html.contains("SHIP FASTER"))
+ `#expect`(html.contains("Clean Design"))

- `#expect`(html.contains("#ff6b6b"))
- `#expect`(html.contains("#feca57"))
+ `#expect`(html.contains("linear-gradient(")) // higher-level background intent

- `#expect`(html.contains("<!DOCTYPE html>"))
- `#expect`(html.contains("9:41"))
+ `#expect`(html.contains("Compare"))

As per coding guidelines Tests/**/*.swift: Test cases must reflect the user's mental model — describe what the user sees and expects, not internal implementation details.

Also applies to: 43-44, 60-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift` around
lines 31 - 33, Replace the fragile html.contains assertions that check
implementation tokens (e.g., html.contains("<!DOCTYPE html>") and
html.contains("container-type")) with user-facing assertions that validate
visible preview content and semantic markers; update the assertions in
TemplatePreviewPageTests (where html is inspected) to assert for expected
headline text, presence of preview sections/devices via stable semantic markers
or ARIA/role attributes, and any sample content a user would see (instead of raw
hex/snippets or "9:41"), so tests use html.contains(...) only for user-visible
strings or stable semantic attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift`:
- Around line 47-63: Update the test `multi-device template preview renders all
device slots` to assert the number of rendered device slots rather than a single
match: after obtaining tmpl.previewHTML in the test, count occurrences of the
wireframe marker (e.g., the "9:41" status bar string) in html and assert the
count equals the number of DeviceSlot entries (2 for the current tmpl). Modify
the assertion that currently uses html.contains("9:41") to a count-based check
(for example using html.components(separatedBy: "9:41").count - 1 or a regex
match count) so the test verifies both DeviceSlot instances are rendered.

---

Nitpick comments:
In `@Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift`:
- Around line 31-33: Replace the fragile html.contains assertions that check
implementation tokens (e.g., html.contains("<!DOCTYPE html>") and
html.contains("container-type")) with user-facing assertions that validate
visible preview content and semantic markers; update the assertions in
TemplatePreviewPageTests (where html is inspected) to assert for expected
headline text, presence of preview sections/devices via stable semantic markers
or ARIA/role attributes, and any sample content a user would see (instead of raw
hex/snippets or "9:41"), so tests use html.contains(...) only for user-visible
strings or stable semantic attributes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7cbe0b68-ea58-46cf-b971-89c78507b062

📥 Commits

Reviewing files that changed from the base of the PR and between 3a25077 and 0e75a91.

📒 Files selected for processing (2)
  • Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift
  • Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/DomainTests/Screenshots/Gallery/GalleryPreviewOutputTests.swift

Comment on lines +47 to +63
@Test func `multi-device template preview renders all device slots`() {
let tmpl = AppShotTemplate(
id: "duo", name: "Duo",
screenLayout: ScreenLayout(
headline: TextSlot(y: 0.04, size: 0.08, preview: "Compare"),
devices: [
DeviceSlot(x: 0.35, y: 0.2, width: 0.6),
DeviceSlot(x: 0.65, y: 0.24, width: 0.6),
]
),
palette: GalleryPalette(id: "d", name: "D", background: "#1a1a2e")
)
let html = tmpl.previewHTML
#expect(html.contains("<!DOCTYPE html>"))
// Both devices should be rendered (as wireframes since no screenshot)
#expect(html.contains("9:41")) // wireframe status bar
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

“All device slots” test currently validates only one rendered slot

The test name promises all device slots are rendered, but html.contains("9:41") only proves at least one match. Please assert the expected count.

✅ Minimal fix
- `#expect`(html.contains("9:41")) // wireframe status bar
+ let renderedWireframes = html.components(separatedBy: "9:41").count - 1
+ `#expect`(renderedWireframes == 2)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/DomainTests/Screenshots/Gallery/TemplatePreviewPageTests.swift` around
lines 47 - 63, Update the test `multi-device template preview renders all device
slots` to assert the number of rendered device slots rather than a single match:
after obtaining tmpl.previewHTML in the test, count occurrences of the wireframe
marker (e.g., the "9:41" status bar string) in html and assert the count equals
the number of DeviceSlot entries (2 for the current tmpl). Modify the assertion
that currently uses html.contains("9:41") to a count-based check (for example
using html.components(separatedBy: "9:41").count - 1 or a regex match count) so
the test verifies both DeviceSlot instances are rendered.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 86.16262% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.58%. Comparing base (f529baa) to head (0e75a91).
⚠️ Report is 84 commits behind head on main.

Files with missing lines Patch % Lines
.../ASCCommand/Commands/AppShots/AppShotsThemes.swift 58.82% 21 Missing ⚠️
Sources/Domain/Screenshots/Gallery/Gallery.swift 85.55% 13 Missing ⚠️
...shotPlans/AggregateGalleryTemplateRepository.swift 0.00% 13 Missing ⚠️
...s/Domain/Screenshots/Gallery/GalleryTemplate.swift 74.28% 9 Missing ⚠️
...main/Screenshots/Gallery/GalleryHTMLRenderer.swift 94.02% 8 Missing ⚠️
Sources/Domain/Screenshots/Gallery/AppShot.swift 86.79% 7 Missing ⚠️
Sources/Domain/Screenshots/ScreenTheme.swift 80.00% 7 Missing ⚠️
...d/Commands/AppShots/AppShotsGalleryTemplates.swift 78.57% 6 Missing ⚠️
...ure/ScreenshotPlans/AggregateThemeRepository.swift 0.00% 6 Missing ⚠️
...rces/Domain/Screenshots/Gallery/HTMLComposer.swift 84.61% 4 Missing ⚠️
... and 2 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #15      +/-   ##
==========================================
+ Coverage   80.54%   80.58%   +0.03%     
==========================================
  Files         331      339       +8     
  Lines        9387     9744     +357     
==========================================
+ Hits         7561     7852     +291     
- Misses       1826     1892      +66     
Files with missing lines Coverage Δ
...CCommand/Commands/AppShots/AppShotsTemplates.swift 65.93% <100.00%> (+2.40%) ⬆️
Sources/Domain/Screenshots/AppShotTemplate.swift 100.00% <100.00%> (ø)
Sources/Domain/Screenshots/AppShotsConfig.swift 100.00% <ø> (ø)
...es/Domain/Screenshots/Gallery/GalleryPalette.swift 100.00% <100.00%> (ø)
...omain/Screenshots/ScreenshotPlans+RESTRoutes.swift 100.00% <ø> (ø)
Sources/Domain/Screenshots/ThemeDesign.swift 100.00% <100.00%> (ø)
...ources/Domain/Screenshots/ThemeDesignApplier.swift 100.00% <100.00%> (ø)
Sources/Domain/Screenshots/ThemedPage.swift 100.00% <100.00%> (ø)
.../ScreenshotPlans/AggregateTemplateRepository.swift 100.00% <100.00%> (ø)
...rces/Domain/Screenshots/Gallery/ScreenLayout.swift 99.15% <99.15%> (ø)
... and 11 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hanrw hanrw merged commit 3b4575b into main Apr 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant