Skip to content

feat(maic-editor): enablement infrastructure (pre-slide-surface)#571

Merged
cosarah merged 2 commits into
feat/maic-editor-v0from
feat/maic-editor-enablement
May 17, 2026
Merged

feat(maic-editor): enablement infrastructure (pre-slide-surface)#571
cosarah merged 2 commits into
feat/maic-editor-v0from
feat/maic-editor-enablement

Conversation

@wyuc
Copy link
Copy Markdown
Contributor

@wyuc wyuc commented May 14, 2026

Implements #570. Lands before #562. Stacked on #568 (the diff against
feat/maic-editor-v0 includes #568's commits until that merges).

What ships

  1. Feature flagNEXT_PUBLIC_MAIC_EDITOR_ENABLED, default OFF.
    Pro toggle in Header only renders when ON. StageMode unchanged.

  2. Slide DSL schema versioningSlideContent.schemaVersion?: number
    (current = 1). migrateSlideContent / migrateScene are pure +
    idempotent; the stage store funnels setScenes / addScene through
    migrateScene so legacy data gets stamped at the boundary.

  3. Round-trip test harnesstests/edit/round-trip/. Caveat: no
    PPTX → Slide reimport exists in the codebase, so the harness does
    apply ops → buildPptxBlob → JSZip parse → assert content survived
    instead of a full reimport diff. Per-op cases extend it in [MAIC Editor] Slide surface — first concrete static-display surface #562.
    buildPptxBlob is now exported (the hook is still the only runtime
    caller).

  4. Edit history persistence — per-scene persist / load / has /
    clear helpers under maic-editor:slide-history:${sceneId}, plus a
    standalone SlideHistoryRestorePrompt dialog. 4 new i18n strings × 6
    locales. Not wired into Stage yet[MAIC Editor] Slide surface — first concrete static-display surface #562 owns the entry effect.

  5. Concurrency guards

    • isSceneEditLocked predicate for the AI-regen lock (defensive;
      current call paths can't structurally trigger it).
    • tryAcquireEditLock / refreshEditLock / releaseEditLock /
      isEditLockHeldByOther for multi-tab, localStorage-backed; atomic
      acquire, idempotent heartbeat, stale-lock takeover after 3×
      heartbeat (default 15s).
    • Standalone MultiTabEditConflictPrompt. 3 new i18n strings × 6
      locales. Not wired into Stage yet — same reason as (4).

Why dialogs aren't wired here

The slide-surface PR owns the edit-entry effect machinery — that's where
the history-state lifecycle and the per-tab tabId ref naturally live.
Shipping half-wired dialogs now means speculatively building Stage state
we'd restructure on contact with the surface.

Out of scope

Server-side history sync (Phase 2); cross-device conflict resolution;
optimistic regen guard.

Test plan

  • prettier · lint (0 new warnings) · tsc · check:i18n-keys · build — green
  • pnpm test410 passing / 9 failing; the 9 are baseline
    tests/server/ssrf-guard.test.ts (DNS-dependent). +45 new tests
    across feature-flag (5), slide-schema (9), round-trip (2),
    slide-history-persistence (8), regen-lock (4), edit-mode-lock (17).
  • Manual: flag unset → no Pro toggle; flag set → Pro toggle behaves
    as in refactor(maic-editor): drop EditModeSidebar; clean Pro mode chrome #568. No new user-visible affordances.

🤖 Generated with Claude Code

@wyuc
Copy link
Copy Markdown
Contributor Author

wyuc commented May 14, 2026

Self-CR pass

Ran a code-review loop. Verdict: READY. Findings:

  • Feature flag correctnessisMaicEditorEnabled parses
    'true'/'1' only; everything else (unset, 'false', 'yes')
    falsy; tests cover all branches. Stage call site at
    components/stage.tsx:1025 passes undefined when off; Header
    gates the Switch on onToggleEditMode &&. ✓
  • Schema migration purity + idempotency — same-reference return
    on already-current input; new object preserving canvas otherwise;
    non-slide scenes pass through identity. Store funneling in
    setScenes / addScene short-circuits via identity equality. ✓
  • Forward-compat fix (CR follow-up commit `0c1b02c`): content
    written with a future schemaVersion >= CURRENT is now returned
    untouched instead of silently downgraded.
  • Round-trip harness honesty — noop test unzips and asserts
    ppt/slides/slide1.xml present (not just blob size); needle test
    uses a distinctive string and asserts .toContain(NEEDLE) against
    the actual slide XML. ✓
  • Storage failure handling — all four persistence helpers wrap
    storage access in try/catch; tests cover corrupted JSON and
    quota-exceeded. Multi-tab lock helpers same. ✓
  • Multi-tab lock semantics — self-renewal allowed; refuses fresh
    other-tab lock; steals after LOCK_STALE_MS; release no-ops
    when another tab now owns; refresh no-ops on other-tab lock.
    Tests cover all branches. ✓
  • No new upstream coupling — new lib/edit/ modules touch only
    the public types (Scene, SlideContent, StageMode,
    SlideEditHistory); EditShell imports unchanged.
  • Not-wired claim is honest — both SlideHistoryRestorePrompt
    and MultiTabEditConflictPrompt are unmounted in this diff. [MAIC Editor] Slide surface — first concrete static-display surface #562
    owns the entry effect.
  • i18n alignment — 7 new keys × 6 locales; check:i18n-keys
    passes.

Tests: 46 new (feature-flag 5 + slide-schema 10 + round-trip 2

  • slide-history-persistence 8 + regen-lock 4 + edit-mode-lock 17),
    all green. 9 pre-existing `tests/server/ssrf-guard.test.ts` failures
    are baseline (DNS-dependent, sandbox-only).

Ready for review.

@wyuc wyuc marked this pull request as ready for review May 14, 2026 01:38
@wyuc wyuc requested a review from cosarah May 14, 2026 01:38
wyuc and others added 2 commits May 13, 2026 21:53
Pre-requisite for the slide surface (#562). Ships the safety
infrastructure so each subsequent surface PR is small and
recoverable:

1. Feature flag NEXT_PUBLIC_MAIC_EDITOR_ENABLED, default OFF —
   gates the Pro toggle in Header. StageMode unchanged.

2. SlideContent.schemaVersion + pure idempotent migrateSlideContent
   / migrateScene; setScenes / addScene funnel legacy data through
   the migrate at the store boundary.

3. tests/edit/round-trip/ harness: apply ops -> buildPptxBlob ->
   JSZip parse -> assert content survived. No PPTX -> Slide reimport
   exists in the codebase, so the full reimport-diff shape isn't
   doable; per-op assertions extend the harness in #562.
   buildPptxBlob is now exported (hook is still the only runtime
   caller).

4. Per-scene slide-history persistence helpers (persist / load /
   has / clear, keyed maic-editor:slide-history:${sceneId}, swallow
   storage failures) + standalone SlideHistoryRestorePrompt dialog
   + 4 new i18n strings x 6 locales. Stage wiring deferred to #562.

5. Concurrency guards: isSceneEditLocked predicate (defensive; no
   current call path structurally hits it); localStorage-backed
   multi-tab edit lock with tryAcquire / refresh / release / heldByOther,
   stale-lock takeover after 3x heartbeat; standalone
   MultiTabEditConflictPrompt + 3 new i18n strings x 6 locales.
   Stage wiring deferred to #562.

The slide-surface PR owns the edit-entry effect machinery (where
the history-state lifecycle and per-tab tabId ref naturally live),
so shipping half-wired dialogs here would speculatively build Stage
state we know we'll restructure on contact with the surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…grade

CR follow-up: previously, content with schemaVersion newer than
CURRENT (e.g. v2 written by a future client) was silently truncated
back to the current version. Now: if schemaVersion >= CURRENT, return
the content untouched. The slide may not render correctly on an older
client, but its on-disk shape stays intact for the next compatible
client to read.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@wyuc wyuc force-pushed the feat/maic-editor-enablement branch from 9746d0a to be675dd Compare May 14, 2026 01:53
Copy link
Copy Markdown
Collaborator

@cosarah cosarah left a comment

Choose a reason for hiding this comment

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

LGTM. Locally verified:

  • Feature flag gates Pro toggle (with/without NEXT_PUBLIC_MAIC_EDITOR_ENABLED)
  • 45 new tests pass (lock, schema migration, history persistence, regen lock, round-trip)
  • Confirmed lock/history APIs are infra-only with no runtime callers yet — matches PR scope note (#562 owns Stage wiring)

Code quality: lock atomicity and stale-takeover logic are sound; migrateSlideContent is idempotent and forward-compatible; localStorage paths are SSR-safe and exception-tolerant. No blocking issues.

@cosarah cosarah merged commit 2ca9329 into feat/maic-editor-v0 May 17, 2026
2 checks passed
wyuc added a commit that referenced this pull request May 17, 2026
)

PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.

- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
  owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
  scene context; geometry drag/resize/rotate commits funnel into
  element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
  off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
  persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
  synchronously inside a pointer interaction; the renderer's
  ResizeObserver text-normalization commits with none, so it is folded
  into the baseline (no undo step / no persist / no spurious restore
  prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
  unit tests; edit.geometry i18n across all 6 locales.

Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 17, 2026
)

PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.

- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
  owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
  scene context; geometry drag/resize/rotate commits funnel into
  element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
  off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
  persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
  synchronously inside a pointer interaction; the renderer's
  ResizeObserver text-normalization commits with none, so it is folded
  into the baseline (no undo step / no persist / no spurious restore
  prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
  unit tests; edit.geometry i18n across all 6 locales.

Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 17, 2026
)

PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.

- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
  owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
  scene context; geometry drag/resize/rotate commits funnel into
  element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
  off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
  persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
  synchronously inside a pointer interaction; the renderer's
  ResizeObserver text-normalization commits with none, so it is folded
  into the baseline (no undo step / no persist / no spurious restore
  prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
  unit tests; edit.geometry i18n across all 6 locales.

Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 17, 2026
) (#579)

PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.

- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
  owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
  scene context; geometry drag/resize/rotate commits funnel into
  element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
  off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
  persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
  synchronously inside a pointer interaction; the renderer's
  ResizeObserver text-normalization commits with none, so it is folded
  into the baseline (no undo step / no persist / no spurious restore
  prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
  unit tests; edit.geometry i18n across all 6 locales.

Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 29, 2026
* feat(maic-editor): enablement infrastructure (pre-slide-surface)

Pre-requisite for the slide surface (#562). Ships the safety
infrastructure so each subsequent surface PR is small and
recoverable:

1. Feature flag NEXT_PUBLIC_MAIC_EDITOR_ENABLED, default OFF —
   gates the Pro toggle in Header. StageMode unchanged.

2. SlideContent.schemaVersion + pure idempotent migrateSlideContent
   / migrateScene; setScenes / addScene funnel legacy data through
   the migrate at the store boundary.

3. tests/edit/round-trip/ harness: apply ops -> buildPptxBlob ->
   JSZip parse -> assert content survived. No PPTX -> Slide reimport
   exists in the codebase, so the full reimport-diff shape isn't
   doable; per-op assertions extend the harness in #562.
   buildPptxBlob is now exported (hook is still the only runtime
   caller).

4. Per-scene slide-history persistence helpers (persist / load /
   has / clear, keyed maic-editor:slide-history:${sceneId}, swallow
   storage failures) + standalone SlideHistoryRestorePrompt dialog
   + 4 new i18n strings x 6 locales. Stage wiring deferred to #562.

5. Concurrency guards: isSceneEditLocked predicate (defensive; no
   current call path structurally hits it); localStorage-backed
   multi-tab edit lock with tryAcquire / refresh / release / heldByOther,
   stale-lock takeover after 3x heartbeat; standalone
   MultiTabEditConflictPrompt + 3 new i18n strings x 6 locales.
   Stage wiring deferred to #562.

The slide-surface PR owns the edit-entry effect machinery (where
the history-state lifecycle and per-tab tabId ref naturally live),
so shipping half-wired dialogs here would speculatively build Stage
state we know we'll restructure on contact with the surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(maic-editor): migrateSlideContent forward-compat — no silent downgrade

CR follow-up: previously, content with schemaVersion newer than
CURRENT (e.g. v2 written by a future client) was silently truncated
back to the current version. Now: if schemaVersion >= CURRENT, return
the content untouched. The slide may not render correctly on an older
client, but its on-disk shape stays intact for the next compatible
client to read.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 29, 2026
) (#579)

PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.

- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
  owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
  scene context; geometry drag/resize/rotate commits funnel into
  element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
  off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
  persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
  synchronously inside a pointer interaction; the renderer's
  ResizeObserver text-normalization commits with none, so it is folded
  into the baseline (no undo step / no persist / no spurious restore
  prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
  unit tests; edit.geometry i18n across all 6 locales.

Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 29, 2026
* feat(maic-editor): enablement infrastructure (pre-slide-surface)

Pre-requisite for the slide surface (#562). Ships the safety
infrastructure so each subsequent surface PR is small and
recoverable:

1. Feature flag NEXT_PUBLIC_MAIC_EDITOR_ENABLED, default OFF —
   gates the Pro toggle in Header. StageMode unchanged.

2. SlideContent.schemaVersion + pure idempotent migrateSlideContent
   / migrateScene; setScenes / addScene funnel legacy data through
   the migrate at the store boundary.

3. tests/edit/round-trip/ harness: apply ops -> buildPptxBlob ->
   JSZip parse -> assert content survived. No PPTX -> Slide reimport
   exists in the codebase, so the full reimport-diff shape isn't
   doable; per-op assertions extend the harness in #562.
   buildPptxBlob is now exported (hook is still the only runtime
   caller).

4. Per-scene slide-history persistence helpers (persist / load /
   has / clear, keyed maic-editor:slide-history:${sceneId}, swallow
   storage failures) + standalone SlideHistoryRestorePrompt dialog
   + 4 new i18n strings x 6 locales. Stage wiring deferred to #562.

5. Concurrency guards: isSceneEditLocked predicate (defensive; no
   current call path structurally hits it); localStorage-backed
   multi-tab edit lock with tryAcquire / refresh / release / heldByOther,
   stale-lock takeover after 3x heartbeat; standalone
   MultiTabEditConflictPrompt + 3 new i18n strings x 6 locales.
   Stage wiring deferred to #562.

The slide-surface PR owns the edit-entry effect machinery (where
the history-state lifecycle and per-tab tabId ref naturally live),
so shipping half-wired dialogs here would speculatively build Stage
state we know we'll restructure on contact with the surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(maic-editor): migrateSlideContent forward-compat — no silent downgrade

CR follow-up: previously, content with schemaVersion newer than
CURRENT (e.g. v2 written by a future client) was silently truncated
back to the current version. Now: if schemaVersion >= CURRENT, return
the content untouched. The slide may not render correctly on an older
client, but its on-disk shape stays intact for the next compatible
client to read.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
wyuc added a commit that referenced this pull request May 29, 2026
) (#579)

PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.

- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
  owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
  scene context; geometry drag/resize/rotate commits funnel into
  element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
  off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
  persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
  synchronously inside a pointer interaction; the renderer's
  ResizeObserver text-normalization commits with none, so it is folded
  into the baseline (no undo step / no persist / no spurious restore
  prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
  unit tests; edit.geometry i18n across all 6 locales.

Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

2 participants