Skip to content

feat: add session snapshot persistence and restoration#7292

Draft
chirag-bruno wants to merge 21 commits intousebruno:mainfrom
chirag-bruno:prototype/snapshot
Draft

feat: add session snapshot persistence and restoration#7292
chirag-bruno wants to merge 21 commits intousebruno:mainfrom
chirag-bruno:prototype/snapshot

Conversation

@chirag-bruno
Copy link
Copy Markdown
Collaborator

Description

Implements automatic session state persistence that saves and restores:

  • Open collections with mount status and collapsed/expanded state
  • Active workspace and environment selections
  • Open tabs with full pane state (active sub-tabs, widths, heights)
  • DevTools state (open/closed, active tab, height)

Key features:

  • Lazy loading: only mounts collections that were previously mounted
  • Stable UIDs: uses hash-based UIDs for reliable item matching across sessions
  • Race condition handling: query-based mount completion tracking
  • Debounced saves: snapshots saved 1 second after state changes
  • Theme-aware loading screen with granular progress messages

Files added:

  • AppSnapshotStore for electron-side persistence
  • Snapshot middleware for Redux state serialization
  • AppLoader component for restore progress display
  • Snapshot utilities for serialization/deserialization

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Implements automatic session state persistence that saves and restores:
- Open collections with mount status and collapsed/expanded state
- Active workspace and environment selections
- Open tabs with full pane state (active sub-tabs, widths, heights)
- DevTools state (open/closed, active tab, height)

Key features:
- Lazy loading: only mounts collections that were previously mounted
- Stable UIDs: uses hash-based UIDs for reliable item matching across sessions
- Race condition handling: query-based mount completion tracking
- Debounced saves: snapshots saved 1 second after state changes
- Theme-aware loading screen with granular progress messages

Files added:
- AppSnapshotStore for electron-side persistence
- Snapshot middleware for Redux state serialization
- AppLoader component for restore progress display
- Snapshot utilities for serialization/deserialization
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ff91757-2d4e-4afa-a900-60f2370b8857

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

cchirag and others added 11 commits February 25, 2026 20:30
…uno#7193)

* fix: skip null query parameters in Postman to Bruno conversion

Updated the importPostmanV2CollectionItem function to skip query parameters where both key and value are null. Added a test case to ensure that such parameters are not included in the converted Bruno collection, while preserving other valid parameters.

* fix: skip null parameters in Postman to Bruno conversion

Updated the importPostmanV2CollectionItem function to skip headers, URL-encoded parameters, and form data where both key and value are null. Added corresponding test cases to ensure proper handling of these scenarios in the conversion process.
* perf: Improve search performance in code editor

Added cache key to reduce duplicate searches over the complete text.
Added incremental update logic, to not update every marked entry when
clicking "next" or "previous".

Fixes: usebruno#6913

* Update delimeter to unicode symbol
…put (usebruno#7114)

* feat: add options to skip request and response bodies in reporter output

- Introduced `--reporter-skip-request-body` and `--reporter-skip-response-body` flags to omit respective bodies from the reporter output.
- Updated examples in the CLI documentation to reflect new options.
- Refactored result sanitization to handle new flags.

* feat: add shorthand option to skip both request and response bodies in reporter output

- Introduced `--reporter-skip-body` as a shorthand for omitting both request and response bodies from the reporter output.
- Updated CLI documentation examples to include the new shorthand option.
- Adjusted result sanitization to accommodate the new option.

* refactor: simplify documentation and tests for reporter-skip-body option

- Updated the description of the `--reporter-skip-body` option to remove redundancy.
- Removed outdated shorthand references from the test suite for clarity.
- Cleaned up examples in the CLI documentation to focus on the current functionality.

* fix: handle optional chaining for request and response properties in result sanitization

- Updated the `sanitizeResultsForReporter` function to use optional chaining when accessing request and response headers and data.
- This change prevents potential errors when these properties are undefined.

* test: enhance reporter-skip-body tests for JSON and HTML outputs

- Added comprehensive tests for the `--reporter-skip-request-body` and `--reporter-skip-response-body` options in both JSON and HTML report formats.
- Verified that the appropriate request and response bodies are included or excluded based on the specified flags.
- Improved test coverage for scenarios where both flags are used simultaneously.

* fix: remove optional chaining for request and response headers in result sanitization

- Updated the `sanitizeResultsForReporter` function to directly assign empty objects to request and response headers, ensuring consistent behavior regardless of their initial state.
- This change simplifies the code and maintains functionality for skipping headers.
* Refactor: optimize collection updates with batch processing

- Introduced BatchAggregator to handle IPC events in batches, reducing Redux dispatch overhead during collection mounting.
- Updated collection watcher to utilize batch processing for adding files and directories, improving UI performance.
- Implemented ParsedFileCacheStore using LMDB for efficient caching of parsed file content, enhancing loading speed and reducing redundant parsing.
- Adjusted collection slice to support batch addition of items, minimizing re-renders and improving state management.
- Updated relevant components to reflect changes in loading states and collection data handling.

* feat: add cache management to preferences

- Introduced a new Cache component in the Preferences section to display cache statistics and allow users to purge the cache.
- Implemented IPC handlers for fetching cache stats and purging the cache in the Electron main process.
- Added styled components for better UI presentation of cache information.
- Updated Preferences component to include a new tab for cache management.

* fix: update package-lock.json to change 'devOptional' to 'dev' for several Babel dependencies

* refactor: update batch aggregation parameters for improved performance

- Increased DISPATCH_INTERVAL_MS from 150ms to 200ms for better timing control.
- Adjusted MAX_BATCH_SIZE from 200 to 300 items to enhance batch processing efficiency.

* feat: enhance collection loading state and improve batch aggregator functionality

- Added isLoading property to collections slice to manage loading state during collection operations.
- Updated getAggregator function calls in collection-watcher to include collectionUid for better context in batch processing.
- Normalized directory path handling in parsed-file-cache to ensure consistent prefix creation for cache keys.

* fix: update loading state and transient file handling in collections slice

- Changed isLoading property to false during collection initialization for accurate loading state representation.
- Introduced isTransient flag for directories and files to differentiate between transient and non-transient items.
- Enhanced logic for handling transient directories and files during collection processing to improve state management.

* feat: add batch processing support for file additions in task middleware

- Implemented a new listener for collectionBatchAddItems to handle batch file additions.
- Enhanced task management by checking for pending OPEN_REQUEST tasks that match added files.
- Improved tab management by dispatching addTab actions for matching files and removing corresponding tasks from the queue.

* feat: enable ASAR packaging and unpacking for LMDB binaries in Electron build configuration

- Added ASAR support to the Electron build configuration for the Bruno application.
- Specified unpacking rules for LMDB native binaries to ensure proper loading during runtime.

* feat: implement parsed file cache using IndexedDB for improved performance

- Introduced a new `parsedFileCacheStore` utilizing IndexedDB for caching parsed file data.
- Replaced the previous LMDB-based cache implementation to enhance performance and reliability.
- Updated IPC handlers to manage cache operations such as get, set, invalidate, and clear.
- Integrated the new cache store into various components, ensuring efficient data retrieval and storage.
- Added pruning functionality to remove outdated cache entries on startup.

* refactor: update collection root and item handling to preserve UIDs

- Modified the way collection roots and folder items are assigned by using `mergeRootWithPreservedUids` and `mergeRequestWithPreservedUids` to ensure UIDs are maintained during updates.
- This change enhances data integrity when managing collections and their associated files.

* refactor: pass mainWindow reference to parsedFileCacheStore initialization

- Updated the `initialize` method in `ParsedFileCacheStore` to accept a `mainWindow` parameter, allowing for direct access to the main window instance in IPC handlers.
- This change improves the handling of IPC requests by ensuring the correct window context is used for sending messages.

* refactor: optimize getStats method in parsedFileCacheStore for performance

- Replaced manual counting of total files with a direct count() call for O(1) performance.
- Updated the collection counting logic to utilize openKeyCursor with 'nextunique' for improved efficiency in counting unique collection paths.
- These changes enhance the performance of the getStats method by reducing the complexity of file and collection counting.

* fix: update key generation in parsedFileCache to use newline separator

- Changed the key generation logic in `generateKey` from a null character to a newline character for improved readability and consistency in cache keys.

* refactor: rename batch-aggregator to collection-tree-batcher and add tests

- Rename BatchAggregator class to CollectionTreeBatcher
- Rename getAggregator/removeAggregator to getBatcher/removeBatcher
- Update imports and variable names in collection-watcher.js
- Add backward-compatible aliases for old names
- Add 22 unit tests covering all functionality

* refactor: update key generation in parsedFileCache to use custom separator

- Changed the key generation logic in `generateKey` to use a custom separator (↝) instead of a newline character for improved readability and consistency in cache keys.

* fix: add missing reject handler and fix directory prefix collision

- Add reject to Promise and pendingRequests in parsed-file-cache-idb.js
- Normalize dirPath with trailing separator in invalidateDirectory to
  prevent false matches (e.g., /foo/bar matching /foo/barley)
- Use platform-specific path.sep for cross-platform compatibility

* fix: add error handling in parsedFileCache and update window close event

- Added a catch block to handle errors in the database promise in parsedFileCache.
- Updated the window close event listener in collection-tree-batcher to use `once` for better resource management.

* fix: add LRU eviction when IndexedDB quota is exceeded

Handle QuotaExceededError in setEntry by automatically evicting
the oldest 20% of cache entries and retrying the write operation.

* fix: use once instead of on in mock window for batcher tests

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
- Add folderUid property for folder-settings tabs in deserialization
- Update restoreTabs reducer to preserve folderUid
- Add folder-settings to TAB_TYPE_TO_SCHEMA mapping
- Refactor waitForMountComplete to use item count stability checking
- Extract createItemsLoadedChecker for reusable load state detection
@chirag-bruno
Copy link
Copy Markdown
Collaborator Author

Session Snapshot Architecture

Overview

The Session Snapshot feature provides automatic persistence and restoration of the user's application state across app restarts. This includes open tabs, active workspace, collection states, selected environments, and DevTools configuration.

Design Goals

  1. Seamless Experience: Users should be able to close and reopen Bruno without losing their working context
  2. Lazy Loading: Collections are mounted on-demand when their workspace is visited, not at startup
  3. Race Condition Safety: Handle async IPC message processing during collection mounting
  4. Minimal Performance Impact: Debounced saves, efficient serialization

Architecture Components

1. Storage Layer (Electron Main Process)

File: packages/bruno-electron/src/store/app-snapshot.js

┌─────────────────────────────────────────────────────────┐
│                    AppSnapshotStore                     │
├─────────────────────────────────────────────────────────┤
│  Storage: ~/Library/Application Support/bruno/         │
│           app-snapshot.json                             │
├─────────────────────────────────────────────────────────┤
│  Methods:                                               │
│    - getSnapshot() → snapshot object                    │
│    - saveSnapshot(snapshot) → persists to disk          │
└─────────────────────────────────────────────────────────┘

Uses electron-store for atomic JSON file operations with automatic error recovery via clearInvalidConfig: true.

2. IPC Handlers

File: packages/bruno-electron/src/ipc/preferences.js

Handler Direction Purpose
renderer:get-app-snapshot Renderer → Main Load snapshot at startup
renderer:save-app-snapshot Renderer → Main Persist snapshot on state changes
renderer:is-collection-mount-complete Renderer → Main Check if collection file watcher has finished

Snapshot Schema (Version 1)

{
  "version": 1,
  "activeWorkspacePathname": "/path/to/workspace",
  "workspaces": [
    {
      "pathname": "/path/to/workspace",
      "collections": [
        {
          "pathname": "/path/to/collection",
          "isMounted": true,
          "isOpen": true,
          "environment": "production",
          "tabs": [...],
          "activeTabIndex": 0
        }
      ]
    }
  ],
  "collections": [...],  // Flattened for quick lookup
  "extras": {
    "devTools": {
      "open": false,
      "tab": "console",
      "height": 300
    }
  }
}

Tab Schema

Tabs are serialized using relative paths to ensure portability:

{
  "type": "item",           // "item" | "preferences" | "runner" | "variables" | etc.
  "permanent": true,        // Not a preview tab
  "itemPath": "folder/request.bru",  // Relative to collection root
  "layout": "horizontal",
  "request": {
    "tab": "body",
    "width": 0,
    "height": 0
  },
  "response": {
    "tab": "response",
    "format": null,
    "preview": false
  }
}

Tab Type Mappings

Redux Type Schema Type Notes
request, http-request, graphql-request, grpc-request, ws-request item All request types serialize as "item"
folder-settings item Folders also use "item" with folder path
collection-settings collection
environment-settings environment
collection-runner runner
variables variables
preferences preferences

Serialization Flow

File: packages/bruno-app/src/utils/app-snapshot/index.js

┌─────────────────────────────────────────────────────────────────┐
│                     serializeAppSnapshot(state)                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Extract state slices:                                       │
│     - collections, tabs, workspaces, logs, app, preferences     │
│                                                                 │
│  2. Filter collections:                                         │
│     - Exclude scratch/transient collections                     │
│     - Identify by scratchCollectionUid or path patterns         │
│                                                                 │
│  3. Group tabs by collection:                                   │
│     - Skip tabs for scratch collections                         │
│     - Build tabsByCollection map                                │
│                                                                 │
│  4. Serialize each collection:                                  │
│     - pathname (absolute)                                       │
│     - isMounted (from mountStatus)                              │
│     - isOpen (inverse of collapsed)                             │
│     - environment (name lookup from activeEnvironmentUid)       │
│     - tabs (serialized via serializeTab)                        │
│     - activeTabIndex                                            │
│                                                                 │
│  5. Preserve pending restores:                                  │
│     - For workspaces not yet visited, keep their restore data   │
│                                                                 │
│  6. Serialize workspaces with collection references             │
│                                                                 │
│  7. Add extras (DevTools state)                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Key Function: serializeTab(tab, collection, preferences)

// For item tabs (requests, folders):
1. Look up item in collection by tab.uid
2. Convert absolute pathname to relative path
3. Capture request/response pane states
4. Return null if item not found (tab won't be saved)

// For non-item tabs (settings, preferences, etc.):
1. Map type to schema type
2. Don't include itemPath

Deserialization Flow

File: packages/bruno-app/src/utils/app-snapshot/restore.js

Key Function: deserializeTab(tabSchema, collection)

┌─────────────────────────────────────────────────────────────────┐
│                    deserializeTab(tabSchema, collection)        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  For non-item tabs:                                             │
│    1. Map schema type → Redux tab type                          │
│    2. Generate UID using collection.uid + suffix                │
│       e.g., "abc123-environment-settings"                       │
│                                                                 │
│  For item tabs:                                                 │
│    1. Find item by relative path (findItemByRelativePath)       │
│    2. If folder: set type to "folder-settings", add folderUid   │
│    3. If request: use item.type (http-request, etc.)            │
│    4. Use item.uid as tab UID                                   │
│    5. Restore request/response pane states                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Tab UID Generation

Tab Type UID Pattern Example
Request/Folder item.uid "a1b2c3d4"
Environment Settings ${collection.uid}-environment-settings "xyz-environment-settings"
Collection Settings ${collection.uid}-collection-settings "xyz-collection-settings"
Variables ${collection.uid}-variables "xyz-variables"
Collection Runner ${collection.uid}-runner "xyz-runner"
Preferences ${collection.uid}-preferences "xyz-preferences"

Restoration Flow

Startup Sequence

┌─────────────────────────────────────────────────────────────────┐
│                        App Startup                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. App mounts, Redux store initializes                         │
│     - snapshotSaveEnabled = false (prevent saves during load)   │
│     - isRestoringSnapshot = false                               │
│                                                                 │
│  2. workspaceOpenedEvent fires for default workspace            │
│     - Sets isInitialLoadComplete = true                         │
│                                                                 │
│  3. restoreAppSnapshot() is dispatched                          │
│     - Sets isRestoringSnapshot = true                           │
│     - Shows AppLoader overlay                                   │
│                                                                 │
│  4. Wait for initial load complete                              │
│                                                                 │
│  5. Load snapshot from main process                             │
│                                                                 │
│  6. Build restore sequence:                                     │
│     - Parse DevTools state                                      │
│     - Identify collections to mount                             │
│     - Map tabs to restore per collection                        │
│                                                                 │
│  7. Restore DevTools state immediately                          │
│                                                                 │
│  8. Set up pending workspace restores:                          │
│     - For each workspace in snapshot, store its restore data    │
│     - Keyed by workspace pathname                               │
│                                                                 │
│  9. Switch to active workspace from snapshot                    │
│     - Triggers restoreWorkspaceState for that workspace         │
│                                                                 │
│  10. Enable snapshot saving                                     │
│      - snapshotSaveEnabled = true                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Lazy Workspace Restoration

File: packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js

┌─────────────────────────────────────────────────────────────────┐
│                  switchWorkspace(workspaceUid)                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Set active workspace                                        │
│                                                                 │
│  2. Load global environments                                    │
│                                                                 │
│  3. Mount scratch collection                                    │
│                                                                 │
│  4. Load workspace collections (lazy mount)                     │
│                                                                 │
│  5. Check for pending restore:                                  │
│     IF pendingWorkspaceRestores[workspace.pathname]:            │
│       → restoreWorkspaceState(collections)                      │
│       → clearPendingWorkspaceRestore                            │
│     ELSE IF workspace not hydrated:                             │
│       → Show default overview tabs                              │
│     ELSE:                                                       │
│       → Focus existing tab from workspace                       │
│                                                                 │
│  6. Mark workspace as hydrated                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Collection Restoration

┌─────────────────────────────────────────────────────────────────┐
│           restoreWorkspaceState(collectionsToRestore)           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  For each collection (parallel, max 4 concurrent):              │
│                                                                 │
│    1. Find or open collection                                   │
│                                                                 │
│    2. Set collapsed state (isOpen)                              │
│                                                                 │
│    3. If was mounted:                                           │
│       a. Trigger mount if not already mounted                   │
│       b. Wait for mount complete (with stability check)         │
│       c. Select environment by name                             │
│       d. Restore tabs via restoreTabsForCollection              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Mount Completion Detection

File: packages/bruno-app/src/utils/collection-mount.js

The Race Condition Problem

When a collection is mounted:

  1. Main process sends IPC messages for each file
  2. Renderer processes messages asynchronously
  3. collection.isLoading becomes false when main process finishes
  4. But renderer may still be processing messages!
Main Process:     [scan files] [send IPCs] [done]
                                            ↓ isLoading = false
Renderer:         [receive] [receive] [receive] [receive] ...
                                       ↑
                        Tab restore attempted here = RACE CONDITION

Solution: Item Count Stability

export const createItemsLoadedChecker = () => {
  let lastItemCount = -1;
  let stableCount = 0;

  return (collection) => {
    // 1. Check mount status and loading state
    if (!collection.mountStatus === 'mounted' || areItemsLoading(collection)) {
      return { ready: false };
    }

    // 2. Check item count stability
    const currentItemCount = flattenItems(collection.items).length;

    if (currentItemCount === lastItemCount) {
      stableCount++;
      if (stableCount >= 3) {  // Stable for 300ms (3 × 100ms polls)
        return { ready: true };
      }
    } else {
      lastItemCount = currentItemCount;
      stableCount = 1;
    }

    return { ready: false, reason: 'stabilizing' };
  };
};

This ensures all IPC messages have been processed before attempting tab restoration.


Save Triggering (Middleware)

File: packages/bruno-app/src/providers/ReduxStore/middlewares/snapshot/middleware.js

Debounced Saves

const DEBOUNCE_DELAY = 1000;  // 1 second

const actionsToIntercept = [
  // Tab actions
  'tabs/addTab', 'tabs/focusTab', 'tabs/closeTabs', ...

  // Workspace actions
  'workspaces/setActiveWorkspace',

  // Collection actions
  'collections/selectEnvironment', 'collections/setCollectionCollapsed', ...

  // DevTools actions
  'logs/openConsole', 'logs/closeConsole', ...
];

// Middleware intercepts these actions and schedules a debounced save

Save Flow

Action dispatched → Middleware intercepts → Schedule save (1s debounce)
                                                    ↓
                                            Save still pending?
                                                    ↓
                                            Serialize state
                                                    ↓
                                            IPC to main process
                                                    ↓
                                            Write to disk

State Management

App Slice Additions

// packages/bruno-app/src/providers/ReduxStore/slices/app.js

initialState: {
  isInitialLoadComplete: false,    // True after default workspace loads
  isRestoringSnapshot: false,      // True during restore (shows loader)
  snapshotRestoreMessage: null,    // Loading message for UI
  snapshotSaveEnabled: false,      // False during restore to prevent saves
  pendingWorkspaceRestores: {},    // Keyed by workspace pathname
}

Tabs Slice Additions

// New action: restoreTabs
// Bulk adds tabs without triggering individual addTab actions
// Handles folderUid for folder-settings tabs

Workspace Hydration Tracking

// packages/bruno-app/src/utils/workspace-hydration.js

// In-memory Set tracking which workspaces have been restored
// Prevents re-applying default tabs when switching back to a workspace

UI Components

AppLoader

File: packages/bruno-app/src/components/AppLoader/index.js

Full-screen overlay shown during snapshot restoration:

  • Theme-aware styling
  • Animated spinner
  • Dynamic message from snapshotRestoreMessage

Edge Cases Handled

1. Deleted Files/Folders

  • findItemByRelativePath returns null
  • Tab is not restored (silently skipped)
  • Warning logged for debugging

2. Renamed Collections

  • Snapshot uses absolute paths
  • Collection won't be found if renamed
  • Opens with default state instead

3. Multiple Workspaces

  • Each workspace has its own pending restore data
  • Lazy loading only restores when workspace is visited
  • Prevents heavy startup load

4. Folder Tabs

  • Folders require folderUid property in tab
  • restoreTabs reducer preserves this property
  • UI looks up folder by focusedTab.folderUid

5. Non-Item Tabs

  • Settings tabs use deterministic UIDs (collection.uid + suffix)
  • Don't require item lookup
  • Always restorable if collection exists

File Summary

File Purpose
utils/app-snapshot/index.js Serialization functions
utils/app-snapshot/restore.js Deserialization functions
utils/collection-mount.js Mount completion detection
utils/workspace-hydration.js Workspace visit tracking
middlewares/snapshot/middleware.js Save triggering
slices/app.js Snapshot state management
slices/tabs.js restoreTabs action
slices/workspaces/actions.js Restore orchestration
components/AppLoader/index.js Loading UI
store/app-snapshot.js (electron) Disk persistence
ipc/preferences.js (electron) IPC handlers

cchirag added 9 commits March 4, 2026 14:06
Replace action-based snapshot tracking with state-based tracking.
Instead of maintaining a list of specific actions to intercept,
the middleware now watches state selectors directly.

This simplifies extending the snapshot to new states - just add
serialization/deserialization and a selector, without needing to
find all actions that mutate the state.
- Add Zod for schema validation
- Create schema/v1.js with complete snapshot schema
- Create schema/index.js with registry and migration runner
- Validate snapshots before saving (catches serialization bugs)
- Validate and migrate snapshots on load (enables version upgrades)

To add a new version: create schema/vN.js, implement migrate in v(N-1).js,
and add to versions array in schema/index.js.
When no tabs are open, show a friendly empty state instead of
"An error occurred!" message. The EmptyState component supports
two modes:
- Normal: "No request open" with guidance to select a request
- Error: Shows warning icon with custom error message for
  malformed tab states
Simplify the snapshot system by removing unnecessary abstractions,
dead code, and improving variable naming for clarity.

Key changes:

Tab UID handling:
- Make all non-item tab UIDs deterministic using ${collectionUid}-${type} pattern
- Remove uuid() calls for collection-runner, variables, openapi-sync, openapi-spec tabs
- Store tab UIDs directly in snapshot instead of deriving from suffix mappings
- Remove itemPath from schema - use uid directly (items can't be reliably restored if moved anyway)

State naming:
- Rename pendingWorkspaceRestores -> deferredWorkspaceSnapshots (clearer intent)
- Rename related actions: setPendingWorkspaceRestore -> setDeferredWorkspaceSnapshot

Code consolidation:
- Delete restore.js, move deserializeTab/deserializeDevTools/restoreTabsForCollection to index.js
- Remove buildRestoreSequence (was mostly dead code, only devTools was used)
- Remove TAB_UID_SUFFIXES mapping (no longer needed with deterministic UIDs)
- Remove getRelativeItemPath and findItemByRelativePath (no longer needed)
- Use existing isScratchCollection from utils/collections instead of local duplicate

Schema simplification:
- Remove itemPath field from tab schema
- Make uid required for all tabs
- Simplify schema validation and migration code

Variable naming improvements:
- mountedCollections -> nonScratchCollections (more accurate)
- collectionSchema -> inline destructuring
- restoreActions -> result
- pendingRestore -> deferredSnapshot

Code style:
- Use for...of instead of forEach where cleaner
- Use spread with && for conditional object properties
- Use nullish coalescing (??) instead of || where appropriate
- Inline simple expressions, reduce intermediate variables

Net reduction: ~436 lines of code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants