Skip to content

fix(desktop): restoreAll reopens every old session as its own window - #5181

Open
imsamad wants to merge 1 commit into
marktext:developfrom
imsamad:fix/restore-all-window-fanout
Open

fix(desktop): restoreAll reopens every old session as its own window#5181
imsamad wants to merge 1 commit into
marktext:developfrom
imsamad:fix/restore-all-window-fanout

Conversation

@imsamad

@imsamad imsamad commented Aug 24, 2026

Copy link
Copy Markdown

Summary

Launching MarkText from a desktop launcher could open a pile of windows — one per leftover
buffer store — instead of restoring the previous session.

A buffer store is only collected once every tab in it reports isSaved. So editing a
file across several sessions without ever hitting save leaves one store behind per session,
and restoreAll opens a window for each. In my case: 34 stores covering 7 distinct files,
one file duplicated 12 times, so 34 windows on launch. The directory grows without bound
because nothing ever removes the stale duplicates.

Only the launcher pathway hits this — passing a file path on the command line skips the
restore branch entirely.

Two changes:

  1. Restore one window per distinct set of open documents, newest wins.
    New EditorBufferStore.getRestorableBufferStores() keys each store by the set of
    pathnames its tabs hold (sorted, so tab order doesn't matter), keeps the store with the
    newest mtime for each key, and deletes the superseded ones so the folder stops growing.
    App.createWindow now calls it instead of iterating getAll().

    Stores containing an untitled tab keep their own identity (untitled:<id>) and are never
    collapsed — an unsaved draft exists nowhere else on disk, so merging two of them would
    lose content.

    A store that fails to parse or stat is skipped, not deleted: it's excluded from the
    restore rather than aborting it, and left on disk rather than thrown away.

  2. Moved buffer cleanup from the mt::close-window handler into forceClose().
    Every window teardown path funnels through forceClose, but the cleanup only ran on the
    renderer's mt::close-window round trip. Windows destroyed any other way —
    forceCloseById, application quit, or a renderer that never answers mt::ask-for-close
    never cleaned up their store, which is a large part of how the duplicates accumulated in
    the first place. The call sits before remove() so the editor-window list still counts
    the closing window.

Type of change

  • Bug fix (non-breaking, fixes an issue)
  • New feature (non-breaking, adds functionality)
  • Breaking change (causes existing functionality to change)
  • Documentation update

Test plan

  • New tests added — packages/desktop/test/unit/specs/buffer-store-restore-dedupe.spec.ts,
    5 cases over getRestorableBufferStores():

    • collapses repeated snapshots of the same document to the newest, deleting the rest
    • keeps one entry per distinct document
    • treats the tab set as the identity regardless of tab order
    • never collapses untitled drafts
    • skips an unparseable store instead of aborting, and leaves it on disk
    pnpm -C packages/desktop exec vitest run test/unit/specs/buffer-store-restore-dedupe.spec.ts
    Test Files  1 passed (1)
         Tests  5 passed (5)
    
  • Manually tested on: Linux — reproduced the 34-window launch, then confirmed a single
    restored window per document set after the fix, with the stale stores gone from the
    buffer-store directory.

Notes for reviewers [optional]

  • Why mtime and not the store id. Ids carry no ordering, so file mtime is the only
    recency signal available for picking a winner among duplicates. It's good enough here:
    the loser is a strictly older snapshot of the same document set.
  • Deletion happens during restore, inside getRestorableBufferStores(). That's a
    side effect in something that reads like a getter, and I'd rather hear if you want it
    split into a separate sweep — I kept them together so the "which store won" decision and
    the "delete the losers" action can't drift apart.
  • Untitled tabs are deliberately never merged, which means a session with an unsaved
    draft still restores as its own window. That's the conservative choice; the alternative
    risks silently dropping unsaved content.
  • The restoreBufferId cast in windowManager.ts is pre-existing — the property is stashed
    on the BrowserWindow and isn't in Electron's types. I moved the call, not the cast.

Buffer stores only get deleted once all their tabs report isSaved. So if
you edit a file across a bunch of sessions without ever hitting save, you
end up with one leftover store per session, and restoreAll opens a window
for each one. I had 34 stores covering 7 files, one of them duplicated 12
times.

Only launching from a desktop launcher hits this, since passing a path on
the command line skips the restore branch entirely.

Now only one store per set of open documents gets restored, the most
recent one, and the older duplicates are deleted so the folder stops
growing. Untitled tabs keep their own identity, there's no file to fall
back on if we merge those.

Also moved the cleanup out of the mt::close-window IPC handler and into
forceClose. Everything goes through forceClose eventually, so windows torn
down by forceCloseById or by quitting the app were never cleaning up.
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