Skip to content

fix(editor): surface failed timeline saves - #308

Merged
EtienneLescot merged 5 commits into
getopenscreen:mainfrom
arhxam:codex/surface-timeline-save-failures
Aug 20, 2026
Merged

fix(editor): surface failed timeline saves#308
EtienneLescot merged 5 commits into
getopenscreen:mainfrom
arhxam:codex/surface-timeline-save-failures

Conversation

@arhxam

@arhxam arhxam commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • route timeline mutation writes through one shared failure boundary
  • show the existing Save failed toast with the native write error while resolving detached mutation promises safely
  • roll optimistic zoom-focus and annotation drags back to their last pre-drag document when the commit fails
  • keep sequential edits usable after a failed write and avoid misleading post-save selection/success updates
  • cover the two metadata-driven saves that previously bypassed the timeline hooks

The issue's detached-call grep now only reports calls whose implementations pass through this resolving boundary; the two direct state.saveDocument results are gone.

Related issue

Fixes #282

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

Not included; the visible change is an existing Sonner error toast when a forced save fails.

Testing

  • Started with failing tests proving both hooks rejected a forced save before the fix
  • npx vitest --run src/lib/ai-edition/store/useTimeline.test.ts src/lib/ai-edition/store/useSequentialTimelineOps.test.ts (26 passed)
  • npm run test (1,679 passed, 1 skipped)
  • npx tsc --noEmit
  • npx tsc -p tsconfig.test.json --noEmit
  • npx biome check on all seven changed files
  • npm run docs:check
  • npm run i18n:check
  • npm run build-vite

Authored with Codex assistance and manually reviewed against every acceptance criterion in #282.

Summary by CodeRabbit

  • Bug Fixes

    • Improved timeline saving reliability with clear error notifications when changes cannot be saved.
    • Prevented unsuccessful edits from leaving the timeline in an inconsistent state by restoring the previous state when needed.
    • Failed operations no longer block subsequent timeline actions.
    • Improved handling for zoom focus, annotations, clips, and regions when saving fails.
  • Tests

    • Added coverage for save failures, state restoration, error notifications, and continued operation processing.

@arhxam
arhxam requested a review from EtienneLescot as a code owner August 8, 2026 18:20
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@EtienneLescot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Limit details: You’ve used all 4 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a2f47-7bcf-4c2a-a3d9-a8a8a56ca412

📥 Commits

Reviewing files that changed from the base of the PR and between ffa0cc9 and 797755d.

📒 Files selected for processing (10)
  • src/components/ai-edition/NewEditorShell.tsx
  • src/components/ai-edition/v4/V4Timeline.tsx
  • src/i18n/toastText.ts
  • src/lib/ai-edition/store/projectStore.test.ts
  • src/lib/ai-edition/store/projectStore.ts
  • src/lib/ai-edition/store/transcriptionStore.ts
  • src/lib/ai-edition/store/useSequentialTimelineOps.test.ts
  • src/lib/ai-edition/store/useSequentialTimelineOps.ts
  • src/lib/ai-edition/store/useTimeline.test.ts
  • src/lib/ai-edition/store/useTimeline.ts
📝 Walkthrough

Walkthrough

Timeline saves now use a shared mutation helper. Save failures show an error toast and return failure results. Sequential operations continue processing. Direct mutations stop dependent state updates and restore live-edit documents when persistence fails.

Changes

Timeline save handling

Layer / File(s) Summary
Shared save mutation contract
src/lib/ai-edition/store/timelineSave.ts, src/lib/ai-edition/store/useSequentialTimelineOps.ts, src/lib/ai-edition/store/useSequentialTimelineOps.test.ts
Centralized save failures into logged error toasts and boolean results. Sequential operations return null on failure while later queued operations continue.
Timeline mutation rollback and guards
src/lib/ai-edition/store/useTimeline.ts, src/lib/ai-edition/store/useTimeline.test.ts
Timeline mutations stop follow-up state changes after failed saves. Live zoom-focus and annotation edits restore their previous documents. Tests cover rollback, unchanged clips, and error toasts.
Editor initialization save integration
src/components/ai-edition/NewEditorShell.tsx
Timeline initialization and probed-duration updates now use the shared save helper.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: etiennelescot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy the coding objectives and acceptance criteria for failed timeline saves in [#282].
Out of Scope Changes check ✅ Passed All changes support centralized timeline save failure handling, rollback behavior, metadata coverage, or related regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely describes the main change: surfacing failed timeline saves.
Description check ✅ Passed The description includes the required summary, issue, change type, release impact, platform impact, screenshots note, and detailed testing information.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/ai-edition/store/useSequentialTimelineOps.test.ts`:
- Around line 148-163: Update the test around the sequential apply calls to
capture the result of the second operation, assert that it is non-null, and
verify that saveDocument receives the second write. Keep the existing
first-result and toast-error assertions, while ensuring the test distinguishes a
genuinely successful second operation from one resolving to null.

In `@src/lib/ai-edition/store/useTimeline.ts`:
- Around line 519-523: Remove the revision increment from both failed-live-edit
rollback setState calls in src/lib/ai-edition/store/useTimeline.ts at lines
519-523 and 631-635. Keep the conditional document match and rollback
document/dirty updates unchanged; these rollback paths must not modify revision.
🪄 Autofix

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 Plus

Run ID: 2fcbfd92-ad30-4fc0-b840-49c1c151e412

📥 Commits

Reviewing files that changed from the base of the PR and between 4e7a85b and f03057b.

📒 Files selected for processing (7)
  • src/components/ai-edition/NewEditorShell.tsx
  • src/components/ai-edition/v4/V4Timeline.tsx
  • src/lib/ai-edition/store/timelineSave.ts
  • src/lib/ai-edition/store/useSequentialTimelineOps.test.ts
  • src/lib/ai-edition/store/useSequentialTimelineOps.ts
  • src/lib/ai-edition/store/useTimeline.test.ts
  • src/lib/ai-edition/store/useTimeline.ts

Comment thread src/lib/ai-edition/store/useSequentialTimelineOps.test.ts Outdated
Comment thread src/lib/ai-edition/store/useTimeline.ts
@arhxam

arhxam commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Removed the unrelated V4Timeline auto-zoom success-toast hunk in ffa0cc9, so this PR is now limited to the timeline save-failure boundary and rollback behavior for #282.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

@arhxam I will review the current PR scope for the timeline save-failure boundary and rollback behavior in #282.

⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EtienneLescot EtienneLescot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at the PR head (ffa0cc9f). Verdict: needs work — one user-visible regression introduced by the third commit, plus the boundary sits one level below where it would actually cover everything.

The saveTimelineMutation boundary and the optimistic-rollback shape are both well built, and the test changes are genuinely strengthened, not weakened — I checked, since the diff shows +/- on existing assertions. The replaced useSequentialTimelineOps assertions swapped firstSettled/expect(err.message) for expect(firstResult).toBeNull() plus a toast assertion plus a new toHaveBeenNthCalledWith(2, secondResult), and commit 2 added the revision === 3 assertion. All three new tests genuinely fail against main. Both test files correctly carry the // @vitest-environment jsdom pragma. Verified locally: both typecheck configs clean, biome clean, 66/66 in store/, 109/109 in components/ai-edition.

1. The third commit's partial revert ships a contradictory toast

chore(editor): keep auto-zoom change scoped reverted the caller-side guard in V4Timeline but kept the producer change in addZoomsBulk (useTimeline.ts:229). The two halves now disagree.

Click Auto-enhance on a project whose file is locked or on a full disk:

  1. addZoomsBulk toasts "Save failed" and returns 0 instead of throwing.
  2. V4Timeline.tsx:1061-1064 then unconditionally runs toast.success(t("toolbar.addedAutoZoomPlural", { count: 0 })).
  3. The user sees "Added 0 automatic zooms" stacked on top of "Save failed", and no zoom exists.

Before this PR the throw reached the catch at V4Timeline:1065 and produced a single correct toolbar.autoZoomFailed toast. This is a regression, and CI cannot see it. Either restore the line commit ffa0cc9f removed:

const added = await tl.addZoomsBulk(suggestions);
if (added === 0) return;

…or make addZoomsBulk keep throwing. The current pairing is the worst of both. Worth a test that mocks a failing save and asserts toast.success is not called.

2. The file this PR fixes still has an uncaught detached save

useTimeline.ts:144-155 — the dimension-backfill effect still calls the raw store save inside void (async () => { … })() with no .catch:

Open a project whose assets have no probed video.width/height (any project migrated from before dimensions were probed) while the project file is read-only → useProjectStore.getState().saveDocument(...) throws → unhandled promise rejection in the renderer, no toast, no log. This is exactly the class the PR header claims to close, and the new memoised saveDocument wrapper at :101 makes the file look uniformly covered, which is how the next reader will miss it.

The effect deliberately reads getState() to stay out of the dep array, so pass the function through the same way NewEditorShell does with persist:

await saveTimelineMutation(useProjectStore.getState().saveDocument, { ...doc });

3. The boundary is one rung below the function every save already routes through

This is the main structural note. projectStore.saveDocument (projectStore.ts:214) is the single function all ~40 save call sites already flow through. Putting the catch+toast there (returning boolean) would be a smaller total diff than the current one and would cover every path. As written, the timeline hook is protected and every sibling writer of the same document is still silent — all reached by void-ed callers, so they also emit unhandled rejections:

  • useEditorSettings.ts:51,68 — callers RightPanes.tsx:1409,1424,1440,1455,1648-1650,1768-1820, PreviewCanvas.tsx:374, V4Timeline.tsx:1360,1404,1422, and EditorTopBar.tsx:240 which does not even void it
  • useCaptions.ts:68,85,94,111 — caption settings and saved translations, via CaptionsPane.tsx:189-510
  • CaptionsPane.tsx:169, LeftPanel.tsx:879, transcriptionStore.ts:325,398
  • NewEditorShell.tsx:680 — picking Save in the unsaved-changes dialog swallows the error in a bare catch {} and resolves "cancel", so the window silently refuses to close with no message

Changing the wallpaper or a caption font on a locked project still loses the edit with zero feedback. Only five callers currently depend on the throw for control flow (NewEditorShell.tsx:322, 617, 652, 680, projectStore.ts:172).

If you would rather keep the scope tight, that is reasonable — but then at minimum route useEditorSettings.commit/set and useCaptions.set/commit/saveTranslation/deleteTranslation through saveTimelineMutation in this PR, so the gap is not silently inherited.

4. Rollback refs are never reset when the project changes

useTimeline.ts:112-115 — the four rollback/live refs outlive the project.

Drag a zoom focus point in project A; ZoomFocusOverlay returns null at :91 the moment focusMode flips to "auto", so endDrag/onFocusCommit never runs and zoomFocusRollbackRef still holds A's document. Open project B, hit the reset-focus button (FloatingInspector.tsx:583-584), and let that save fail: the state.document === doc check passes and the store is set to project A's document while projectId is B. The next successful save writes A's content into B.

Secondary cost: two full AxcutDocument snapshots pinned indefinitely per hook instance, and annotations can carry base64 image data URLs.

useEffect(() => {
  zoomFocusRollbackRef.current = null;
  zoomFocusLiveRef.current = null;
  annotationRollbackRef.current = null;
  annotationLiveRef.current = null;
}, [projectId]);

…or guard the rollback with rollback.project.id === doc.project.id.

5. Rollback asserts a cleanliness it cannot prove

useTimeline.ts:518-524 and :630-636 set dirty: false unconditionally, but the rollback target is not guaranteed to be the last saved document.

Type into an annotation → commitAnnotationChange fires, its save still in flight → type again → updateAnnotationLive sets annotationRollbackRef = D1 (the unsaved doc from the first, failed commit) → the second save also fails → the store is set to D1 with dirty: false. dirty is the only input to beforeunload (NewEditorShell.tsx:283) and to electronAPI.setHasUnsavedChanges (:297), so the app now claims there is nothing to save and closes without prompting, dropping D1. Same shape with an interleaved useEditorSettings.setLive.

Simplest fix is to drop dirty: false from both updaters — leaving dirty as-is errs toward prompting, which is the safe direction.

6. Hardcoded English string in a 13-locale app

timelineSave.ts:18-21 raises toast.error("Save failed", …) in English. Every other toast from the timeline UI (V4Timeline, FloatingInspector) goes through t()/ts(). A French user gets "Save failed" next to "Zoom automatique ajouté", and npm run i18n:check cannot catch it because no locale file is touched — so it ships.

The module is outside a component, so either take the message as a parameter, or move the toast to the already-localised call sites. At minimum add the key to src/i18n/locales/*/timeline.json.

Nits

  • timelineSave.ts:16-22 vs NewEditorShell.tsx:613-624handleSave already contains the identical toast.error("Save failed", { description: err instanceof Error ? err.message : String(err) }) block, verbatim, and was not switched over. It also loses the new console.error breadcrumb. if (await saveTimelineMutation(saveDocument, doc)) toast.success("Project saved");
  • useSequentialTimelineOps.ts:29,52-56apply() now returns null for both "no project loaded" (a no-op) and "the save failed" (user-visible error), with no way to distinguish them. Fine today since both callers void it. Also, the header comment now claims it "keeps detached UI calls from emitting unhandled rejections" — that is only true for save failures; a throw from applyTimelineOperation or the dynamic import() still rejects queued, and both call sites are void-ed (NewEditorShell.tsx:559, 573).
  • useTimeline.test.ts — the new describe("useTimeline save failures") block has a beforeEach but no afterEach(() => vi.clearAllMocks()), unlike every sibling describe. Harmless only because it is currently last in the file.
  • Pre-existing but adjacent to this PR's theme: NewEditorShell.tsx:1301-1302 — Apply in the Edit Clip modal fires two concurrent saves built from the same stale document, so changing both the source range and the crop silently loses one. With this PR the failure case also stacks two identical "Save failed" toasts for one click. This is the exact race useSequentialTimelineOps exists to prevent; those two calls just don't go through it.

§1 is the one I'd call blocking — it's a regression this PR introduces. §2 and §3 are the difference between "the timeline hook is covered" and the PR's stated goal.

@EtienneLescot

Copy link
Copy Markdown
Collaborator

Filed the Edit Clip double-save race from my review as #355 — pre-existing and distinct from #282, tracked separately.

@EtienneLescot
EtienneLescot force-pushed the codex/surface-timeline-save-failures branch from ffa0cc9 to 0a381c5 Compare August 20, 2026 11:46
@EtienneLescot

Copy link
Copy Markdown
Collaborator

Picking this up — @arhxam hasn't come back on the review. Pushed to the same branch, rebased onto main (183 commits behind; clean). #282 is still open.

§1 — the contradictory toast. Blocking, and fixed. runAutoZooms guards on added === 0 again, so a locked file no longer produces "Added 0 automatic zooms" stacked on the save failure. The producer's half of the contract is now pinned by a test: addZoomsBulk returns 0 when the write fails, and removing that guard fails it.

§3 — the boundary. Taken, and it drove the rest. The catch, the log and the toast are in projectStore.saveDocument now, which returns boolean and never rejects. timelineSave.ts is deleted. It is a smaller diff than the wrapper it replaces, and it covers every path — including useEditorSettings, useCaptions, CaptionsPane, LeftPanel and transcriptionStore, all of which were silent.

§2 — falls out of §3. The dimension-backfill effect calls the store directly, so it is covered without the effect being touched.

§4 — rollback refs outliving the project. Fixed, and it is the finding I would have been most annoyed to ship: your walkthrough reproduces. Cleared on projectId change. There is a test that restores into a different project and asserts it does not take.

§5 — dirty: false asserting a cleanliness it cannot prove. Dropped, in the rollback and in the store's failure path. dirty is the only input to beforeunload and setHasUnsavedChanges, so a failed write is the last moment to claim there is nothing to save.

§6 — hardcoded English. Gone. The toast goes through the existing project.failedToSave key. transcriptionStore's outside-React translate helper moved to @/i18n/toastText so the project store could share it rather than grow a second copy.

Nits. NewEditorShell.handleSave's duplicate block is gone, along with the local "Save failed" / "Rename failed" toasts — they were English-only copies of what the store now says in the user's language. useSequentialTimelineOps' header comment no longer overclaims, and its null return documents both meanings explicitly. afterEach(vi.clearAllMocks) added. The Edit Clip double-save race is left alone: pre-existing, tracked as #355.

On the five throw-dependent callers. None lost behaviour — the close handler still answers false, the unsaved dialog still cancels, Ctrl+N and Ctrl+O still stay put. Two of those were catch {} blocks that would have gone dead silently; they read the boolean now.

Merge order with #309. #309 adds a try/catch rollback around saveDocument in agentDocumentApply.ts. If this lands first, that catch goes dead. Whichever merges second needs ~3 lines adjusting — happy to do that rebase.

Verification. 1810 passed / 5 skipped; both typecheck configs, biome, i18n and docs clean, and no new biome warnings (12 before and after). Mutation-tested: making saveDocument report success on failure fails 4 tests, dropping the ref reset fails 1, dropping the addZoomsBulk guard fails 1.

arhxam and others added 5 commits August 20, 2026 15:08
`transcriptionStore` had the only copy of "read the stored locale, validate
it, translate" -- the thing any store needs to report to the user in their
language. The project store is about to need it too, so it moves to
`@/i18n/toastText` and takes the namespace as an argument instead of
hardcoding `editor`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four findings from the review, all of them real.

**The boundary was one rung too low.** `saveTimelineMutation` wrapped the
timeline hook, so the timeline was covered while every sibling writer of
the same document stayed silent -- and they are all reached by `void`-ed
callers, so they emitted unhandled rejections with no toast, no log and no
clue. Changing the wallpaper or a caption font on a read-only project lost
the edit with zero feedback, through `useEditorSettings`, `useCaptions`,
`CaptionsPane`, `LeftPanel` and `transcriptionStore`. Picking Save in the
unsaved-changes dialog swallowed the error in a bare `catch {}` and
resolved "cancel", so the window silently refused to close.

`projectStore.saveDocument` is the one function all of them already call.
The catch, the log and the toast live there now and it returns a boolean
instead of throwing, which covers every path -- including the two direct
store saves the hook could never see, one of which is the dimension
backfill that fires on any project migrated from before dimensions were
probed. `timelineSave.ts` is gone; this is a smaller diff than the wrapper
it replaces. `dirty` is deliberately left set on failure: it is the only
input to the `beforeunload` guard and to `setHasUnsavedChanges`, so a
failed write is the last moment to claim there is nothing to save.

The five callers that used the throw for control flow now read the
boolean, and none of them lost behaviour: the close-request handler still
answers false, the unsaved dialog still cancels, Ctrl+N and Ctrl+O still
stay put. Their local "Save failed" / "Rename failed" toasts are gone --
a second, English-only copy of what the store now says in the user's
language, which is the other half of this: the toast read `"Save failed"`
hardcoded in a 13-locale app, and no locale file was touched, so
`i18n:check` could not see it. It goes through `project.failedToSave` now.

**Auto-enhance congratulated itself on a failed write.** `addZoomsBulk`
was changed to toast and return 0 instead of throwing, but its caller kept
toasting success unconditionally -- so a locked file produced "Added 0
automatic zooms" stacked on the failure, with no zoom anywhere. Before,
the throw reached the caller's `catch` and produced one correct message.
The caller guards on 0 again, and the producer's contract is now pinned by
a test.

**A rollback outlived its project.** `ZoomFocusOverlay` unmounts the
instant `focusMode` flips to "auto", so `endDrag` never runs and the
pre-drag snapshot stayed in the ref. Open another project, reset the focus,
let that save fail, and project A's document was restored into project B --
the next successful save then wrote A over B on disk. The refs are cleared
when the project changes, which also stops pinning two whole documents per
hook instance when annotations can carry base64 image data URLs.

**And the rollback claimed a cleanliness it could not prove.** It set
`dirty: false`, but the rollback target is the document the drag started
from, not the last SAVED one: with two commits in flight the first one's
unsaved document is what gets restored. Saying "clean" there let the window
close on real work without prompting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@EtienneLescot
EtienneLescot force-pushed the codex/surface-timeline-save-failures branch from 0a381c5 to 797755d Compare August 20, 2026 13:17
@EtienneLescot
EtienneLescot merged commit c5afdd1 into getopenscreen:main Aug 20, 2026
16 checks passed
EtienneLescot added a commit that referenced this pull request Aug 20, 2026
#308 and #309 merged twelve seconds apart and their contracts disagree.
`projectStore.saveDocument` returns a boolean and never rejects now, so
the `try`/`catch` #309 put around it went dead: nothing type-checks
wrong, no test in either PR could see it, and `main` went red on the one
test that could.

What shipped: an agent edit whose write failed stayed on screen with
`dirty` set, was never rolled back, and `applyAgentDocumentIfCurrent`
returned "applied" for a write that never happened. The next unrelated
save would then persist the document we had just told the user was
rejected -- the exact failure #309 exists to prevent, reintroduced by
landing its fix next to the one that changed the signature underneath it.

The check is on the returned `false` now, and "save-failed" is its own
result rather than being folded into "conflict": the two need different
words. A conflict means the project moved and the turn is still in hand,
so it offers "Apply anyway"; a failed write means the disk refused, and
retrying it would just fail again.

`LeftPanel` says what it cost without repeating why -- the store already
toasted the native error -- because the assistant's "done, I removed 14
silences" renders either way, and a bare save error next to it leaves the
two unconnected. Its `catch` stays for the one throw left on this path,
a malformed document from the agent, and no longer mislabels it a
conflict.

Both halves are pinned: removing the rollback fails the test, and so does
returning "applied" without checking.

Co-Authored-By: Claude Opus 5 <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.

[Bug]: a failed timeline save is invisible — the edit stays on screen, the document is not written

2 participants