Skip to content

feat(SUP-23): implement session storage for pending capture state restoration#104

Open
arjun-veer wants to merge 5 commits intomainfrom
feat/persist-auto-capture
Open

feat(SUP-23): implement session storage for pending capture state restoration#104
arjun-veer wants to merge 5 commits intomainfrom
feat/persist-auto-capture

Conversation

@arjun-veer
Copy link
Contributor

No description provided.

Copilot AI review requested due to automatic review settings February 9, 2026 11:27
@linear
Copy link

linear bot commented Feb 9, 2026

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • Pending capture sessions persist to session storage with versioning, expiry and domain/title awareness and can be restored after refresh.
  • Bug Fixes

    • Pending state is saved/cleared consistently across show/hide/error paths.
    • Capture toggle now initializes to false to avoid UI inconsistencies.
  • Reliability

    • Better validation and debug logging around state restoration, storage interactions and cleanup.

Walkthrough

Adds session-scoped persistence and restore for pending capture state to CaptureMemoryManager, centralizes rendering/state handling, ensures pending state is saved/cleared on show/hide/error, integrates restoration into content script startup, tightens cleanup logging, and makes captureEnabled default to false in settings.

Changes

Cohort / File(s) Summary
Capture memory manager
src/entrypoints/content/components/capture-memory-manager.tsx
Adds versioned StoredCaptureState, storage constants (STORAGE_KEY, STORAGE_VERSION, MAX_AGE_MS), pendingCaptureStorage handle, savePendingState() / clearPendingState(), isValidStoredState() validation, static restoreIfNeeded(ctx) to reconstruct and re-show pending captures, caches siteTitle/siteDomain, centralizes render() and updates lifecycle to persist/clear pending state; adds storage error logging and domain-safe UI fallbacks.
Content script startup / teardown
src/entrypoints/content/index.ts
Calls CaptureMemoryManager.restoreIfNeeded(ctx) during startAutoCapture, uses restored instance if present (avoids re-instantiation), and replaces silent cleanup catches with logger.debug logging.
Capture settings UI
src/components/features/setting/capture-settings.tsx
Changes captureEnabled initial state from `boolean

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ContentScript as Content Script
    participant MemoryMgr as CaptureMemoryManager
    participant Storage as sessionStorage
    participant UI as Modal UI

    ContentScript->>MemoryMgr: restoreIfNeeded(ctx)
    MemoryMgr->>Storage: read(STORAGE_KEY)
    alt stored valid state
        MemoryMgr->>MemoryMgr: isValidStoredState(data)
        MemoryMgr->>MemoryMgr: construct manager (cache title/domain)
        MemoryMgr->>MemoryMgr: show() -> savePendingState()
        MemoryMgr->>UI: render() with cached title/domain
        MemoryMgr-->>ContentScript: return restored instance
    else no/invalid state
        MemoryMgr-->>ContentScript: return null
        ContentScript->>MemoryMgr: create new instance (if needed)
    end

    User->>UI: interact (submit/hide/error)
    UI->>MemoryMgr: hide()/error() -> clearPendingState()
    MemoryMgr->>Storage: remove(STORAGE_KEY)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • mikr13
  • pratham-kpatil
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether the description relates to the changeset. Add a description explaining the purpose of session storage persistence, the restoration mechanism, and any relevant context from the associated ticket (SUP-23).
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing session storage for pending capture state restoration, which aligns with the primary objective of adding persistence mechanisms and state restoration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/persist-auto-capture

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/entrypoints/content/index.ts (1)

147-165: ⚠️ Potential issue | 🔴 Critical

Restored captureMemoryManager is immediately overwritten on line 165.

After restoration (lines 147–153), execution falls through because fieldTracker, submissionMonitor, and captureService are still null — so the early-return guard at lines 138–145 doesn't trigger. Line 165 then unconditionally creates a new CaptureMemoryManager(), discarding the just-restored instance. The restored modal becomes orphaned (visible but unmanageable).

Proposed fix — skip re-creation when restored manager exists
       if (!captureMemoryManager) {
         const restored = await CaptureMemoryManager.restoreIfNeeded(ctx);
         if (restored) {
           captureMemoryManager = restored;
           logger.info("Restored pending capture modal from storage");
         }
       }

       logger.info("Initializing memory capture for site:", {
         hostname,
         pathname,
         reason,
       });

       try {
         fieldTracker = await getFieldDataTracker();
         submissionMonitor = getFormSubmissionMonitor();
         captureService = new CaptureService();
-        captureMemoryManager = new CaptureMemoryManager();
+        if (!captureMemoryManager) {
+          captureMemoryManager = new CaptureMemoryManager();
+        }

         submissionMonitor.start();
🤖 Fix all issues with AI agents
In `@src/entrypoints/content/components/capture-memory-manager.tsx`:
- Around line 490-504: The isValidStoredState type guard currently only checks
capturedFields is a non-empty array; update isValidStoredState (and the
StoredCaptureState validation) to validate the shape of capturedFields entries
by checking that at least the first element (or all elements) is an object with
required properties (e.g., fieldOpid is a string/number as expected, question is
a string, answer is a string or null/undefined per model) and that their types
are correct before returning true; reference the isValidStoredState function and
the capturedFields array and ensure this stronger validation prevents passing
malformed data into saveCapturedMemories or render paths.
- Around line 473-478: restoreIfNeeded sets manager.siteTitle/siteDomain but
calling CaptureMemoryManager.show() then unconditionally overwrites them (and
calls savePendingState()) — change show() to avoid re-reading the DOM and
re-saving when we are restoring: either add an optional parameter (e.g.,
show({skipDomRestore: true}) or isRestore flag) or check existing values inside
show() before assigning from document.title/window.location.hostname, and skip
calling savePendingState() when that flag/condition indicates a restoration;
update callers (the restore path that sets manager.siteTitle/siteDomain) to call
show with the restore flag or rely on the guarded assignments so the original
captured title/domain are preserved.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements restoration of the “pending capture” modal after reload/navigation by persisting the modal state and attempting to restore it when the content script initializes auto-capture.

Changes:

  • Add a restore hook in the content script startup flow to re-open a pending capture modal if persisted state exists.
  • Persist/clear pending capture modal state from the capture UI manager and add restoration/validation logic (versioning + expiry).
  • Refactor CaptureMemoryManager.render to rely on stored siteTitle/siteDomain fields rather than passing them as parameters.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/entrypoints/content/index.ts Attempts to restore a pending capture modal before initializing auto-capture services.
src/entrypoints/content/components/capture-memory-manager.tsx Adds pending modal state persistence + restore/validation logic, and refactors render to use stored site metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/entrypoints/content/index.ts (1)

419-421: 🧹 Nitpick | 🔵 Trivial

captureMemoryManager.hide() is not awaited in onInvalidated.

While hide() is declared async, its internals (clearPendingState via sessionStorage.removeItem, ui.remove()) are synchronous, so this works in practice. However, if hide() ever gains truly async work, this would silently skip cleanup. Consider adding void to signal intentional fire-and-forget:

Proposed fix
     if (captureMemoryManager) {
-      captureMemoryManager.hide();
+      void captureMemoryManager.hide();
     }
🤖 Fix all issues with AI agents
In `@src/entrypoints/content/components/capture-memory-manager.tsx`:
- Around line 398-416: In savePendingState(), stop using sessionStorage and
instead persist the same StoredCaptureState via WXT's storage API: call
storage.defineItem("session:captureState") (or the existing STORAGE_KEY prefixed
with "session:") and use its set method to store the JSON/state; update
savePendingState to be async (or return the Promise) and await the storage call,
and convert the existing try/catch to handle Promise rejections and log errors
via logger.error as before. Locate the savePendingState method in
CaptureMemoryManager (and adjust any corresponding loadPendingState logic to
read from storage.get or the defined item) so persistence is moved from
page-accessible sessionStorage to browser.storage.session using the session:
prefix.

Copilot AI review requested due to automatic review settings February 9, 2026 14:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/entrypoints/content/components/capture-memory-manager.tsx`:
- Around line 213-225: The guard in show() uses falsy checks on this.siteTitle
and this.siteDomain which treats legitimate empty strings (e.g., document.title
=== "") as unset; change to use explicit sentinel/flag: initialize
this.siteTitle and this.siteDomain to undefined (or add boolean flags like
this.siteTitleSet) and in show() check against undefined (or check the flag)
before assigning document.title/window.location.hostname, and ensure restored
empty titles/domains are preserved; keep savePendingState() call as-is.
- Line 36: Remove the dead constant STORAGE_KEY which is no longer used; locate
the top-level const STORAGE_KEY = "superfill:pendingCaptureModal" in
capture-memory-manager.tsx and delete it (the code now uses
pendingCaptureStorage which contains its own key string), and run a quick grep
for STORAGE_KEY to ensure there are no remaining references before committing.
- Around line 418-424: Remove the dead QuotaExceededError branch and simplify
the catch in the save/persist logic so it matches browser.storage.session
failure modes: stop checking error instanceof Error && error.name ===
"QuotaExceededError" and instead treat all rejections as a single catch-all,
logging the error via logger.error with a clear message (e.g., "Failed to save
pending capture state") and the error object/string; reference the existing
logger.error call and the storage usage of browser.storage.session to locate and
replace the conditional branch in the catch block.
- Around line 483-488: The restore flow re-saves pending state with a fresh
timestamp because CaptureMemoryManager.show() always calls savePendingState(),
so restoreIfNeeded (which sets manager.siteTitle/siteDomain and calls
manager.show()) extends TTL; modify show() and/or savePendingState() to accept a
flag or optional originalTimestamp so that when restoreIfNeeded calls show() it
either skips re-saving or re-saves using pendingState.timestamp; update
restoreIfNeeded to pass that flag/original timestamp (reference:
restoreIfNeeded, CaptureMemoryManager.show, savePendingState,
pendingState.timestamp) so restored state keeps its original timestamp or avoids
writing a new one.
- Around line 406-417: Change the noisy success-path logs from info to debug for
internal bookkeeping in the capture memory manager: in savePendingState()
replace the logger.info("Saved pending capture modal state...") call with a
debug-level log, and likewise in the corresponding clearPendingState() method
replace its logger.info(...) call (the "Cleared pending capture modal state..."
message) with logger.debug(...); this keeps normal hide/save flows quiet while
preserving the messages for troubleshooting.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 10, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/entrypoints/content/index.ts (1)

410-441: ⚠️ Potential issue | 🟡 Minor

Unhandled promise from captureMemoryManager.hide() during invalidation; inconsistent error handling.

Two concerns:

  1. Fire-and-forget hide() (line 435): hide() is async and calls clearPendingState(), but onInvalidated doesn't await it. During extension reload/update, the clearPendingState storage write may not complete, leaving orphaned state in browser.storage.session. For normal page navigation this is actually desirable (state should persist for restoration), but during invalidation the intent appears to be cleanup.

  2. Empty catch blocks (lines 413, 417, 422, 440) contrast with the improved logger.debug logging in stopAutoCapture. Consider applying the same logger.debug pattern here for consistency, since these are the same kinds of cleanup operations.

Proposed improvement
     ctx.onInvalidated(() => {
       try {
         formDetectionService.dispose();
-      } catch {}
+      } catch (error) {
+        logger.debug("Error disposing form detection service:", error);
+      }
 
       try {
         unwatchCaptureSettings?.();
-      } catch {}
+      } catch (error) {
+        logger.debug("Error unwatching capture settings:", error);
+      }
       unwatchCaptureSettings = null;
 
       try {
         unlistenSubmission?.();
-      } catch {}
+      } catch (error) {
+        logger.debug("Error removing submission listener:", error);
+      }
       unlistenSubmission = null;
 
       if (fieldTracker) {
         fieldTracker.dispose();
       }
       if (submissionMonitor) {
         submissionMonitor.dispose();
       }
       if (captureService) {
         captureService.dispose();
       }
       if (captureMemoryManager) {
-        captureMemoryManager.hide();
+        void captureMemoryManager.hide();
       }
 
       try {
         cleanupGuideListener();
-      } catch {}
+      } catch (error) {
+        logger.debug("Error cleaning up guide listener:", error);
+      }
     });

Using void on line 435 makes the intentional fire-and-forget explicit (suppresses the floating promise lint), and the logger.debug additions maintain consistency with stopAutoCapture.

🤖 Fix all issues with AI agents
In `@src/entrypoints/content/components/capture-memory-manager.tsx`:
- Around line 456-464: The code currently constructs new
URL(pendingState.siteUrl) which can throw for malformed siteUrl; update
validation in isValidStoredState to verify pendingState.siteUrl is a well-formed
URL (e.g., attempt new URL(...) inside that validator or use a safe URL-regex)
so invalid stored states are rejected earlier, or add a small try-catch around
new URL(pendingState.siteUrl) before comparing hostnames to convert malformed
values into the same stale-state-clearing path
(pendingCaptureStorage.setValue(null) and logger.info) without bubbling to the
outer error log; reference pendingState.siteUrl, isValidStoredState,
pendingCaptureStorage.setValue, and logger.info when making the change.
- Around line 482-490: The assignment to manager.currentFields is redundant
because CaptureMemoryManager.show() already sets currentFields from the passed
capturedFields; remove the line "manager.currentFields =
pendingState.capturedFields" and keep the instantiation and the call to
manager.show(ctx, pendingState.capturedFields, { skipPersist: true }); ensure
manager.siteTitle and manager.siteDomain assignments remain as they are.

Comment on lines +456 to +464
const currentUrl = window.location.href;
const pendingUrl = new URL(pendingState.siteUrl);
const currentUrlObj = new URL(currentUrl);

if (pendingUrl.hostname !== currentUrlObj.hostname) {
await pendingCaptureStorage.setValue(null);
logger.info("Cleared stale pending capture from different domain");
return null;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

new URL() on stored siteUrl can throw on malformed data — handled by outer catch, but consider validating earlier.

If a corrupted siteUrl passes the typeof === "string" check in isValidStoredState but isn't a valid URL, new URL(pendingState.siteUrl) on line 457 throws. The outer try-catch handles it gracefully (clears state and returns null), so this isn't a functional bug, but it means a recoverable validation failure takes the same heavy error-logging path as an unexpected crash.

Proposed enhancement — validate URL in isValidStoredState
   private static isValidStoredState(data: unknown): data is StoredCaptureState {
     if (typeof data !== "object" || data === null) return false;
 
     const state = data as Record<string, unknown>;
 
+    // Validate siteUrl is a parseable URL
+    let parsedUrl: URL;
+    try {
+      parsedUrl = new URL(state.siteUrl as string);
+    } catch {
+      return false;
+    }
+
     return (
       Array.isArray(state.capturedFields) &&
       typeof state.siteUrl === "string" &&
       typeof state.siteTitle === "string" &&
🤖 Prompt for AI Agents
In `@src/entrypoints/content/components/capture-memory-manager.tsx` around lines
456 - 464, The code currently constructs new URL(pendingState.siteUrl) which can
throw for malformed siteUrl; update validation in isValidStoredState to verify
pendingState.siteUrl is a well-formed URL (e.g., attempt new URL(...) inside
that validator or use a safe URL-regex) so invalid stored states are rejected
earlier, or add a small try-catch around new URL(pendingState.siteUrl) before
comparing hostnames to convert malformed values into the same
stale-state-clearing path (pendingCaptureStorage.setValue(null) and logger.info)
without bubbling to the outer error log; reference pendingState.siteUrl,
isValidStoredState, pendingCaptureStorage.setValue, and logger.info when making
the change.

Comment on lines +482 to +490
const manager = new CaptureMemoryManager();
manager.currentFields = pendingState.capturedFields;
manager.siteTitle = pendingState.siteTitle;
manager.siteDomain = pendingState.siteDomain;

await manager.show(ctx, pendingState.capturedFields, {
skipPersist: true,
});
return manager;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant assignment of currentFields before show().

Line 483 assigns manager.currentFields, but show() (line 217) immediately overwrites it with the same value. The assignment is harmless but unnecessary.

Proposed cleanup
       const manager = new CaptureMemoryManager();
-      manager.currentFields = pendingState.capturedFields;
       manager.siteTitle = pendingState.siteTitle;
       manager.siteDomain = pendingState.siteDomain;
🤖 Prompt for AI Agents
In `@src/entrypoints/content/components/capture-memory-manager.tsx` around lines
482 - 490, The assignment to manager.currentFields is redundant because
CaptureMemoryManager.show() already sets currentFields from the passed
capturedFields; remove the line "manager.currentFields =
pendingState.capturedFields" and keep the instantiation and the call to
manager.show(ctx, pendingState.capturedFields, { skipPersist: true }); ensure
manager.siteTitle and manager.siteDomain assignments remain as they are.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +514 to +526
state.capturedFields.every(
(f: unknown) =>
typeof f === "object" &&
f !== null &&
typeof (f as Record<string, unknown>).fieldOpid === "string" &&
typeof (f as Record<string, unknown>).formOpid === "string" &&
typeof (f as Record<string, unknown>).question === "string" &&
typeof (f as Record<string, unknown>).answer === "string" &&
typeof (f as Record<string, unknown>).timestamp === "number" &&
typeof (f as Record<string, unknown>).wasAIFilled === "boolean" &&
typeof (f as Record<string, unknown>).fieldMetadata === "object" &&
(f as Record<string, unknown>).fieldMetadata !== null,
)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The validation function doesn't check for optional fields in CapturedFieldData. According to the CapturedFieldData interface, fields like originalAIValue and aiConfidence are optional, and fieldMetadata has nested optional properties like placeholder. The validation should handle these optional fields gracefully rather than rejecting stored data that lacks them. Consider checking that these fields are either undefined or have the correct type when present.

Suggested change
state.capturedFields.every(
(f: unknown) =>
typeof f === "object" &&
f !== null &&
typeof (f as Record<string, unknown>).fieldOpid === "string" &&
typeof (f as Record<string, unknown>).formOpid === "string" &&
typeof (f as Record<string, unknown>).question === "string" &&
typeof (f as Record<string, unknown>).answer === "string" &&
typeof (f as Record<string, unknown>).timestamp === "number" &&
typeof (f as Record<string, unknown>).wasAIFilled === "boolean" &&
typeof (f as Record<string, unknown>).fieldMetadata === "object" &&
(f as Record<string, unknown>).fieldMetadata !== null,
)
state.capturedFields.every((f: unknown) => {
if (typeof f !== "object" || f === null) {
return false;
}
const field = f as Record<string, unknown>;
if (
typeof field.fieldOpid !== "string" ||
typeof field.formOpid !== "string" ||
typeof field.question !== "string" ||
typeof field.answer !== "string" ||
typeof field.timestamp !== "number" ||
typeof field.wasAIFilled !== "boolean"
) {
return false;
}
if (
typeof field.fieldMetadata !== "object" ||
field.fieldMetadata === null
) {
return false;
}
const fieldMetadata = field.fieldMetadata as Record<string, unknown>;
// Optional top-level field: originalAIValue (string when present)
if (
"originalAIValue" in field &&
field.originalAIValue !== undefined &&
typeof field.originalAIValue !== "string"
) {
return false;
}
// Optional top-level field: aiConfidence (number when present)
if (
"aiConfidence" in field &&
field.aiConfidence !== undefined &&
typeof field.aiConfidence !== "number"
) {
return false;
}
// Optional nested field: fieldMetadata.placeholder (string when present)
if (
"placeholder" in fieldMetadata &&
fieldMetadata.placeholder !== undefined &&
typeof fieldMetadata.placeholder !== "string"
) {
return false;
}
return true;
})

Copilot uses AI. Check for mistakes.
Comment on lines +514 to +526
state.capturedFields.every(
(f: unknown) =>
typeof f === "object" &&
f !== null &&
typeof (f as Record<string, unknown>).fieldOpid === "string" &&
typeof (f as Record<string, unknown>).formOpid === "string" &&
typeof (f as Record<string, unknown>).question === "string" &&
typeof (f as Record<string, unknown>).answer === "string" &&
typeof (f as Record<string, unknown>).timestamp === "number" &&
typeof (f as Record<string, unknown>).wasAIFilled === "boolean" &&
typeof (f as Record<string, unknown>).fieldMetadata === "object" &&
(f as Record<string, unknown>).fieldMetadata !== null,
)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The validation function doesn't validate the nested fieldMetadata properties according to the CapturedFieldData interface. The interface specifies that fieldMetadata should have properties like type, purpose, labels (array), required (boolean), and optional placeholder. Consider adding validation for these specific properties to ensure the stored state matches the expected structure.

Suggested change
state.capturedFields.every(
(f: unknown) =>
typeof f === "object" &&
f !== null &&
typeof (f as Record<string, unknown>).fieldOpid === "string" &&
typeof (f as Record<string, unknown>).formOpid === "string" &&
typeof (f as Record<string, unknown>).question === "string" &&
typeof (f as Record<string, unknown>).answer === "string" &&
typeof (f as Record<string, unknown>).timestamp === "number" &&
typeof (f as Record<string, unknown>).wasAIFilled === "boolean" &&
typeof (f as Record<string, unknown>).fieldMetadata === "object" &&
(f as Record<string, unknown>).fieldMetadata !== null,
)
state.capturedFields.every((f: unknown) => {
if (typeof f !== "object" || f === null) {
return false;
}
const field = f as Record<string, unknown>;
if (
typeof field.fieldOpid !== "string" ||
typeof field.formOpid !== "string" ||
typeof field.question !== "string" ||
typeof field.answer !== "string" ||
typeof field.timestamp !== "number" ||
typeof field.wasAIFilled !== "boolean"
) {
return false;
}
const meta = field.fieldMetadata as Record<string, unknown> | null | undefined;
if (typeof meta !== "object" || meta === null) {
return false;
}
if (
typeof meta.type !== "string" ||
typeof meta.purpose !== "string" ||
!Array.isArray(meta.labels) ||
!meta.labels.every((label: unknown) => typeof label === "string") ||
typeof meta.required !== "boolean"
) {
return false;
}
if (
"placeholder" in meta &&
meta.placeholder !== undefined &&
typeof meta.placeholder !== "string"
) {
return false;
}
return true;
})

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +178
if (!captureMemoryManager) {
const restored = await CaptureMemoryManager.restoreIfNeeded(ctx);
if (restored) {
captureMemoryManager = restored;
logger.info("Restored pending capture modal from storage");
}
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

There's a potential race condition when restoration is attempted before other services are initialized. If restoreIfNeeded() successfully restores the manager and calls show(), but then the subsequent initialization of fieldTracker, submissionMonitor, or captureService fails (line 186-189), the restored UI will remain visible but the capture functionality won't be fully operational. Consider handling this case by hiding the restored manager if initialization fails.

Copilot uses AI. Check for mistakes.

await manager.show(ctx, pendingState.capturedFields, {
skipPersist: true,
});
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

If manager.show() fails (line 487), the error is caught internally by show() which doesn't re-throw, and the manager is still returned on line 490. This means restoreIfNeeded() returns a manager instance that failed to display the UI. The caller in index.ts assigns this manager to captureMemoryManager, but it's not in a usable state. Consider checking manager.isVisible after calling show() and returning null if the UI failed to display, or have show() re-throw errors during restoration.

Suggested change
});
});
if (!manager.isVisible) {
logger.error(
"CaptureMemoryManager.show did not display the UI during restoration; clearing pending state",
);
try {
await pendingCaptureStorage.setValue(null);
} catch (cleanupError) {
logger.debug("Error clearing corrupted state:", cleanupError);
}
return null;
}

Copilot uses AI. Check for mistakes.
}

const currentUrl = window.location.href;
const pendingUrl = new URL(pendingState.siteUrl);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The URL construction on lines 457-458 can throw an error if pendingState.siteUrl is not a valid URL. While this is caught by the outer try-catch, it would be better to validate the URL before attempting to parse it, or handle the specific error case with a more informative log message. This would help distinguish between corrupted URL data versus other types of errors.

Suggested change
const pendingUrl = new URL(pendingState.siteUrl);
let pendingUrl: URL;
try {
pendingUrl = new URL(pendingState.siteUrl);
} catch (parseError) {
logger.warn("Invalid pending capture siteUrl, clearing", {
siteUrl: pendingState.siteUrl,
error:
parseError instanceof Error ? parseError.message : String(parseError),
});
await pendingCaptureStorage.setValue(null);
return null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +464
if (pendingUrl.hostname !== currentUrlObj.hostname) {
await pendingCaptureStorage.setValue(null);
logger.info("Cleared stale pending capture from different domain");
return null;
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

When restoring state, the code only checks if the hostname matches (line 460) but doesn't validate that the current page is suitable for showing the capture modal. If the user navigates from one page to another on the same domain where capture is disabled (e.g., due to settings changes or messaging sites), the modal will still be restored. Consider adding a check using shouldAutoCaptureForCurrentPage() before restoring the modal.

Copilot uses AI. Check for mistakes.
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