Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,32 @@ This file provides guidance to Claude Code and other AI assistants working on th
SuperDeck is a Flutter presentation framework that renders slides written in Markdown. Users write slides in a `slides.md` file using Markdown syntax with custom block annotations, and SuperDeck renders them as a Flutter application.

- **Live demo**: https://superdeck-dev.web.app
- **Repository**: https://github.com/btwld/superdeck
- **Repository**: https://github.com/conceptadev/superdeck

## Project Structure

This is a Melos monorepo with the following packages:

```
packages/
core/ # Rendering primitives, Markdown parsing, schema validation (Dart-only)
core/ # Shared deck models, schemas, Markdown utilities, storage contracts (Dart-only)
superdeck/ # Flutter widgets and presentation components
cli/ # superdeck CLI tool (setup, build, publish, version)
builder/ # Code generators and build_runner integration
cli/ # superdeck CLI tool (setup, build/watch, version)
builder/ # Deck parsing, serialization, and build/watch pipeline (Dart-only)
playground/ # Flutter authoring app, editor, and AI generation
plugins/pdf/ # PDF export plugin
demo/ # Sample presentation app
docs/ # User-facing documentation (MDX format)
```

### Key Package Responsibilities

- **core**: Markdown processing, slide/block configuration, shared model/schema validation, YAML utilities (no Flutter dependency)
- **core**: Shared Markdown utilities, slide/block configuration, model/schema validation, storage contracts, and YAML utilities (no Flutter dependency)
- **superdeck**: Flutter widgets, DeckController, navigation, thumbnail/capture services, theme system
- **cli**: CLI commands for project setup and building slides
- **builder**: build_runner generators for code generation
- **builder**: Markdown deck parsing, serialization, build/watch orchestration, and build plugins
- **playground**: Deck authoring, editor/file sessions, AI generation, and theme customization
- **plugins/pdf**: PDF capture and export UI

## Environment Setup

Expand All @@ -52,23 +56,51 @@ Always work inside the FVM-provided SDK (`.fvm/flutter_sdk`) to avoid toolchain
fvm dart run melos run analyze # Run dart analyze + DCM analysis
fvm dart run melos run analyze:all # Full analysis including unused code/files
fvm dart run melos run fix # Apply dart fix + DCM autofixes
fvm dart run melos run custom_lint_analyze # Run custom lint rules
```

### Code Generation
```bash
fvm dart run melos run build_runner:build # Generate code (run before tests)
fvm dart run melos run build_runner:watch # Watch mode for development
fvm dart run melos run build_runner:clean # Clean generated files
fvm dart run melos run brb # Alias for build_runner:build
fvm dart run melos run brbc # Alias for build_runner:clean
```

### Testing

Each command covers one layer. No single command covers every layer.

```bash
fvm dart run melos run test # Run all tests
fvm dart run melos run test:coverage # Run tests with coverage
fvm flutter test <path> # Run specific test file
fvm dart run melos run test # Package unit and widget tests
fvm dart run melos run test:integration # Desktop integration tests (Linux)
fvm dart run melos run test:integration:macos # Desktop integration tests (macOS)
fvm dart run melos run test:e2e:web # Browser smoke tests (Chromium, WebKit)
fvm dart run melos run test:e2e # Linux integration + browser smoke
fvm dart run melos run test:all # Package tests + Linux integration tests
fvm dart run melos run test:coverage # Package tests with coverage
fvm flutter test <path> # One test file
```

`melos run test` excludes `ci-excluded` suites; `test:coverage` includes them.
No melos command runs the live generation tests. Run those from
`packages/playground`:

```bash
# Deterministic checkpoint, no provider call.
fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \
--dart-define=LIVE_FAKE_CHECKPOINT=true --reporter expanded

# Live 10-slide smoke test; requires the repository-root .env file.
fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \
--dart-define=LIVE_FIXTURE=superdeck_demo_10 \
--dart-define-from-file=../../.env --reporter expanded
```

The live cases skip when the supplied defines contain no `GOOGLE_AI_API_KEY`.
A missing define file fails before the tests start. Omitting `LIVE_FIXTURE`
selects the default small-fixture suite rather than the 10-slide checkpoint.

### Running Apps & Live Debugging
```bash
cd packages/playground
Expand Down Expand Up @@ -104,7 +136,7 @@ fvm dart run melos run clean # Clean all Flutter build artifacts
- Two-space Dart indentation
- `snake_case.dart` filenames
- Prefer relative imports over package imports
- Avoid exporting from entry-point files
- Avoid importing the package's own entry-point file internally; import the defining file
- Keep widgets focused; colocate private helpers with their widget
- Run `melos run fix` before committing

Expand Down Expand Up @@ -159,7 +191,7 @@ lib/src/
- Unit tests live under each package's `test/` directory
- Always regenerate code before running tests
- Add regression tests with bug fixes
- CI blocks merges on failing analyze/test jobs
- Require passing analysis and relevant test checks before merging

## Commit Guidelines

Expand Down Expand Up @@ -219,6 +251,8 @@ Styles are defined in Dart through `SlideStyler`, `DeckOptions.baseStyle`, and `
| Bootstrap workspace | `fvm dart run melos bootstrap` |
| Run all analysis | `fvm dart run melos run analyze` |
| Generate code | `fvm dart run melos run build_runner:build` |
| Run tests | `fvm dart run melos run test` |
| Run package tests | `fvm dart run melos run test` |
| Run desktop integration tests | `fvm dart run melos run test:integration:macos` |
| Run browser smoke tests | `fvm dart run melos run test:e2e:web` |
| Apply fixes | `fvm dart run melos run fix` |
| Clean workspace | `fvm dart run melos run clean` |
90 changes: 90 additions & 0 deletions docs/generation_and_rendering_review_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Remaining Markdown review work

Updated September 8, 2026. The generation and runtime cleanup is implemented
in the PR stack below. The Markdown renderer comparison and parser assembly
review remain outstanding in this workspace, so this plan is still needed.

This replaces the original broad review proposal with its remaining work.
The separate renderer handoff contains the detailed architecture and source
context. No Markdown implementation change is part of the cleanup stack.

## Completed cleanup

| Area | Implementation |
| --- | --- |
| Capture teardown, thumbnail pruning, transition ownership, and CI for stacked PRs | [PR #108](https://github.com/conceptadev/superdeck/pull/108) |
| Shared generation application, cancellation and document/file revision guards, service setup, and editor notices | [PR #109](https://github.com/conceptadev/superdeck/pull/109) |
| Build-status ownership, loader snapshots, script aliases, and test-command documentation | [PR #110](https://github.com/conceptadev/superdeck/pull/110) |

These changes retain the existing package boundaries and generation models.
Generation has passed the deterministic checkpoint and a live 10-slide run
with three artworks, 18.778 seconds of generation time, and ten captured slides.
That validates one generation run; it does not establish renderer parity or
replace the separate interactive authoring checks.

## 1. Establish the Markdown behavior contract

Inventory the behavior supplied by the existing renderer and each custom
builder. Start with the
[Markdown showcase](../packages/core/test/fixtures/markdown/github_web_markdown_showcase.md)
and existing parser, codec, widget, and capture tests.

Cover headings, paragraphs, inline formatting, nested lists, tasks, tables,
alerts, blockquotes, fenced code, Mermaid, images, custom widgets, Unicode,
text scaling, and overflow. Define intended link activation, selection, and
inline-image behavior before changing those features.

Deliver a feature-to-test matrix that distinguishes current behavior from
approved changes. Reuse existing fixtures and assertions.

## 2. Compare rendering the existing AST with ordinary widgets

Keep the Dart `markdown` parser and the current `flutter_markdown_plus`
implementation as the working baseline. Begin the separate comparison with
paragraphs, headings, and nested alerts rendered from existing parsed nodes.
Preserve Mix styling, inline formatting, Hero behavior, asset resolution, and
syntax configuration. Do not introduce another AST or a permanent renderer
selection framework for the experiment.

Compare the maintained code each approach needs, parser invocations during
style changes, cold/warm render cost, transition frames, capture completion,
and memory for representative 10- and 20-slide decks. Use matching SDK, fonts,
assets, devices, build modes, and cache conditions.

A successful text prototype is not a complete renderer. Before selecting or
extending it, evaluate lists, tables, code, blockquotes, images, semantics,
links, and selection on macOS and web. Investigate another widget package only
if the comparison leaves a specific requirement unresolved.

Deliver a keep, simplify, or replace decision supported by screenshots,
interaction results, measurements, and maintenance cost. A replacement needs
an identifiable benefit and coverage of every affected contract.

## 3. Review shared deck assembly

The CLI builder and Playground codec still assemble slide models separately
from the same deck parsers. Compare both paths before plugins run, using the
same inputs: options, sections, comments, widget arguments, slide identity,
fenced directives, malformed edits, and error diagnostics.

If parity supports consolidation, extract one small pure assembly function in
`builder`. Keep filesystem access, plugins, status reporting, and each caller's
recovery behavior with their current owners. Preserve serializer normalization
and slide-identity contracts. This is a separate parser change from selecting
a content renderer.

## Validation and retirement

Use the layer-specific commands in [AGENTS.md](../AGENTS.md). Regenerate code,
run analysis and contract checks, then run affected package, macOS integration,
and Chromium/WebKit tests. Capture output is part of renderer acceptance.

For each Markdown change, verify the complete authoring path: CLI build,
opening a deck, editing, invalid-edit recovery, saving/reloading, navigation,
and thumbnail/PDF capture. Keep `fvm flutter run` attached during manual checks
and inspect its logs after interactions. Run live generation again when its
behavior or model configuration changes.

Retire this plan when the renderer decision and parser consolidation have
been implemented or explicitly declined, with the decisions and validation
recorded in their PRs or maintained architecture documentation.
36 changes: 31 additions & 5 deletions packages/builder/lib/src/build/deck_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ class DeckBuilder {
final slides = await build();
yield BuildCompleted(slides.toList());
} catch (e, stackTrace) {
await store.saveBuildStatus(
phase: DeckBuildPhase.failure,
error: e,
stackTrace: stackTrace,
);
// [build] already published the failure status for this build.
yield BuildFailed(e, stackTrace);
}
}
Expand All @@ -103,7 +99,37 @@ class DeckBuilder {
return buildFuture;
}

/// Runs one build and publishes its status.
///
/// This is the single owner of the build status, so a direct build and a
/// watch build report a failure the same way. The original build error
/// always reaches the caller, even when the status write fails too.
Future<Iterable<Slide>> _build() async {
try {
return await _runBuild();
} catch (error, stackTrace) {
await _publishBuildFailure(error, stackTrace);
rethrow;
}
}

Future<void> _publishBuildFailure(Object error, StackTrace stackTrace) async {
try {
await store.saveBuildStatus(
phase: DeckBuildPhase.failure,
error: error,
stackTrace: stackTrace,
);
} catch (statusError, statusStackTrace) {
_logger.warning(
'Could not record the failed build status.',
statusError,
statusStackTrace,
);
}
}

Future<Iterable<Slide>> _runBuild() async {
_logger.info('Starting build...');
await store.initialize();
await store.saveBuildStatus(phase: DeckBuildPhase.building);
Expand Down
120 changes: 120 additions & 0 deletions packages/builder/test/src/build/deck_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,90 @@ Discuss release plan.
);
});

test('publishes failure status for a direct build', () async {
const markdown = '# First Slide\n\nOriginal content';
final builder = DeckBuilder(
workspace: workspace,
store: store,
plugins: [
_TransformPlugin(
id: 'test.direct-failure',
transform: (_, _) => throw StateError('transform failed'),
),
],
);

await workspace.slidesFile.writeAsString(markdown);
await expectLater(() => builder.build(), throwsA(isA<Exception>()));

final status = await _readBuildStatus(workspace);
expect(status.phase, DeckBuildPhase.failure);
expect(status.error?.message, contains('transform failed'));
});

test('a later successful build replaces the failure status', () async {
const markdown = '# First Slide\n\nOriginal content';
var shouldFail = true;
final builder = DeckBuilder(
workspace: workspace,
store: store,
plugins: [
_TransformPlugin(
id: 'test.recovering',
transform: (block, _) {
if (shouldFail) throw StateError('transform failed');

return block;
},
),
],
);

await workspace.slidesFile.writeAsString(markdown);
await expectLater(() => builder.build(), throwsA(isA<Exception>()));
expect((await _readBuildStatus(workspace)).phase, DeckBuildPhase.failure);

shouldFail = false;
await builder.build();

final status = await _readBuildStatus(workspace);
expect(status.phase, DeckBuildPhase.success);
expect(status.slideCount, 1);
expect(status.error, isNull);
});

test('keeps the build error when the status write also fails', () async {
const markdown = '# First Slide\n\nOriginal content';
final failingStore = _FailingStatusStore(
workspace: workspace,
failOn: DeckBuildPhase.failure,
);
final builder = DeckBuilder(
workspace: workspace,
store: failingStore,
plugins: [
_TransformPlugin(
id: 'test.status-write-failure',
transform: (_, _) => throw StateError('transform failed'),
),
],
);

await workspace.slidesFile.writeAsString(markdown);

await expectLater(
() => builder.build(),
throwsA(
isA<Exception>().having(
(error) => error.toString(),
'message',
contains('transform failed'),
),
),
);
expect(failingStore.failureWriteAttempts, 1);
});

test('preserves DeckFormatException thrown by plugins', () async {
const markdown = '# First Slide\n\nOriginal content';
final builder = DeckBuilder(
Expand Down Expand Up @@ -643,3 +727,39 @@ final class _FailingReferenceStore extends DeckBuildStore {
await onSaveReferences();
}
}

Future<DeckBuildStatus> _readBuildStatus(DeckWorkspace workspace) async {
final decoded =
jsonDecode(await workspace.buildStatusJson.readAsString())
as Map<String, dynamic>;

return DeckBuildStatus.fromJson(decoded);
}

/// Store that refuses to record one build phase.
final class _FailingStatusStore extends DeckBuildStore {
_FailingStatusStore({required super.workspace, required this.failOn});

final DeckBuildPhase failOn;
var failureWriteAttempts = 0;

@override
Future<void> saveBuildStatus({
required DeckBuildPhase phase,
int? slideCount,
Object? error,
StackTrace? stackTrace,
}) async {
if (phase == failOn) {
failureWriteAttempts++;
throw const FileSystemException('Cannot write the build status.');
}

return super.saveBuildStatus(
phase: phase,
slideCount: slideCount,
error: error,
stackTrace: stackTrace,
);
}
}
Loading
Loading