Broaden mobile download fallbacks by cbkii · Pull Request #33 · cbkii/userscripts · GitHub
Skip to content

Broaden mobile download fallbacks#33

Merged
cbkii merged 5 commits into
mainfrom
cx/fix-download-features-in-xbrowser
Jan 10, 2026
Merged

Broaden mobile download fallbacks#33
cbkii merged 5 commits into
mainfrom
cx/fix-download-features-in-xbrowser

Conversation

@cbkii

@cbkii cbkii commented Jan 10, 2026

Copy link
Copy Markdown
Owner

User description

Motivation

  • Address silent download failures on mobile devices where blob: URLs or GM_download callbacks/promises may not behave reliably.
  • Broaden the previous XBrowser-specific workaround to target a wider set of mobile browsers (Android/iOS).
  • Preserve existing GM and anchor/data-URL fallback chains while adding a mobile-specific timed fallback to avoid hangs.

Description

  • Replaced isXBrowser with isMobileBrowser and use a broader user-agent check /Android|iPhone|iPad|iPod|Mobile/i across pagemd.user.js, pageinfoexport.user.js, and chatgptmd.user.js, and bumped script versions.
  • Renamed XBROWSER_FALLBACK_DELAY_MS to MOBILE_FALLBACK_DELAY_MS and added mobile-only fallback timers around GM_download that mark blobs stale and trigger the existing fallback flow when the timer elapses.
  • For mobile flows, prefer converting small blobs to data URLs (using a MAX_DATA_URL_FILE_SIZE_BYTES threshold) and set confirm: false in GM_download details to improve compatibility with mobile download managers.
  • Adjusted promise handling so non-mobile flows still await download promises but mobile flows rely on the timeout/fallback path, and added a blobToDataUrl helper in chatgptmd.

Testing

  • Automated tests: none were executed for these changes.

Codex Task


PR Type

Enhancement, Bug fix


Description

  • Replace XBrowser-specific checks with broader mobile browser detection

  • Add mobile-specific download timeout fallback to prevent hangs

  • Prefer data URL conversion for small blobs on mobile devices

  • Set confirm: false in GM_download for improved mobile compatibility

  • Adjust promise handling to use timeout-based fallback on mobile


Diagram Walkthrough

flowchart LR
  A["Mobile Browser Detected"] --> B["Convert Blob to Data URL<br/>if size &lt; 2MB"]
  B --> C["GM_download with<br/>confirm: false"]
  C --> D["Set Timeout<br/>MOBILE_FALLBACK_DELAY_MS"]
  D --> E["Timeout Triggers<br/>Fallback Chain"]
  E --> F["Anchor Download<br/>or Data URL Download"]
  C --> G["onload/onerror<br/>Callbacks"]
  G --> H["Clear Timeout<br/>& Cleanup"]
Loading

File Walkthrough

Relevant files
Enhancement
chatgptmd.user.js
Add mobile download timeout fallback with data URL conversion

chatgptmd.user.js

  • Added isMobileBrowser detection using user-agent regex
  • Introduced MOBILE_FALLBACK_DELAY_MS constant and
    MAX_DATA_URL_FILE_SIZE_BYTES threshold
  • Created blobToDataUrl helper function for async blob-to-data-URL
    conversion
  • Modified mobileDownload to be async and implement timeout-based
    fallback logic
  • Added mobile-specific handling: convert small blobs to data URLs, set
    confirm: false, and use timeout fallback instead of awaiting promises
  • Updated version to 2026.01.10.0804
+57/-11 
pageinfoexport.user.js
Implement mobile timeout fallback in GM_download flow       

pageinfoexport.user.js

  • Added isMobileBrowser detection using user-agent regex
  • Introduced MOBILE_FALLBACK_DELAY_MS constant for timeout-based
    fallback
  • Modified saveWithGMDownload to implement mobile-specific timeout
    fallback with promise handling
  • Added mobileFallback promise that resolves after timeout on mobile
    devices
  • Set confirm: false in download details for mobile browsers
  • Changed promise handling to skip await on mobile, relying on timeout
    fallback instead
  • Updated version to 2026.01.10.0804
+61/-6   
pagemd.user.js
Add mobile timeout fallback to GM_download handler             

pagemd.user.js

  • Added isMobileBrowser detection using user-agent regex
  • Introduced MOBILE_FALLBACK_DELAY_MS constant for timeout-based
    fallback
  • Modified downloadViaGM to implement mobile-specific timeout fallback
    logic
  • Added clearFallbackTimer helper and timeout-based fallback on mobile
    devices
  • Set confirm: false in download details for mobile browsers
  • Changed promise handling to skip await on mobile, using timeout
    fallback instead
  • Updated version to 2026.01.10.0804
+34/-5   

Summary by CodeRabbit

  • New Features

    • Unified mobile-aware download flow with automatic fallbacks and small-file data-URL support to improve downloads on mobile browsers.
    • Mobile-specific download behaviour to reduce prompts and improve compatibility.
  • Bug Fixes

    • Improved timeout, fallback coordination and cleanup to avoid stalled downloads, duplicate fallbacks and resource leaks.
    • More robust blob-to-data-URL handling with graceful fallbacks.
  • Chores

    • Script version bumps.

✏️ Tip: You can customize this high-level summary in your review settings.

Manual test:
- On a mobile browser (Android/iOS), export Page → Markdown and confirm the download completes.
- On a mobile browser, export Page Info and confirm the .txt download completes.
- On a mobile browser, export a ChatGPT conversation and confirm the download completes.
@coderabbitai

coderabbitai Bot commented Jan 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Three userscripts add mobile-aware download handling: mobile detection, a mobile fallback timer, blob→data-URL conversion for small blobs, unified fallback/cleanup logic, and altered async download control flow to coordinate GM_download vs data/blob URL fallbacks while preserving desktop behaviour.

Changes

Cohort / File(s) Summary
Mobile-aware download core
chatgptmd.user.js, pagemd.user.js, pageinfoexport.user.js
Add isMobileBrowser, MOBILE_FALLBACK_DELAY_MS, fallback timer logic (triggerFallback / clearFallbackTimer), and apply confirm: false for mobile GM_download; coordinate legacy vs mobile completion paths.
Blob / Data-URL handling
chatgptmd.user.js, pageinfoexport.user.js, pagemd.user.js
Introduce MAX_DATA_URL_FILE_SIZE_BYTES and blobToDataUrl(blob); attempt blob→dataURL for blobs under size limit and fall back to blob URLs on failure.
Async control flow changes
chatgptmd.user.js, pagemd.user.js, pageinfoexport.user.js
Make internal mobile download path promise-aware; desktop awaits download promise as before, mobile uses a fallback timer to trigger fallback paths if GM_download stalls.
Timing & cleanup adjustments
chatgptmd.user.js, pagemd.user.js, pageinfoexport.user.js
Centralise revoke/cleanup logic and coordinate resolve paths across legacy GM_download, mobile fallback, and data-URL fallbacks to ensure proper cleanup and signalling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Script
    participant GM_download
    participant Timer

    User->>Script: Request export/download
    Script->>Script: Detect isMobileBrowser, create Blob, check size
    alt Mobile & blob < MAX_SIZE
        Script->>Script: blobToDataUrl(blob)
        Script->>User: Trigger download via data URL
        Script->>Script: Revoke / cleanup
    else Mobile & blob >= MAX_SIZE
        Script->>GM_download: Start GM_download (confirm:false)
        Script->>Timer: Start MOBILE_FALLBACK_DELAY_MS
        par
            GM_download-->>Script: onload/onerror
            Script->>Timer: clearFallbackTimer()
            Script->>Script: Cleanup / resolve
        and
            Timer->>Script: timeout -> triggerFallback()
            Script->>Script: Fallback -> data/blob URL download
            Script->>Script: Cleanup / resolve
        end
    else Desktop
        Script->>GM_download: Start GM_download (normal)
        GM_download-->>Script: Complete
        Script->>Script: Cleanup
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through blobs and timers, light and spry,

I turned small bits to URLs that fly.
On mobile sands I set a tiny clock,
If GM stalls, a fallback hop — tick tock.
Downloads land safe — a cheerful little hop.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Broaden mobile download fallbacks' accurately summarizes the main change: replacing XBrowser-specific logic with broader mobile detection and adding mobile-specific fallback mechanisms.

✏️ 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 cx/fix-download-features-in-xbrowser

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73ebe3a and 16088b3.

📒 Files selected for processing (3)
  • chatgptmd.user.js
  • pageinfoexport.user.js
  • pagemd.user.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.user.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.user.js: Userscript metadata block must be positioned at the very beginning of the file with exact syntax: // ==UserScript== and // ==/UserScript== on their own lines, each metadata line starting with // (exactly one space after //)
Include required metadata keys: @name, @namespace (set to https://github.com/cbkii/userscripts), @version (datetime format YYYY.MM.DD.HHMM), @description, @author, @icon (base64 SVG data URI with #FF1493 hot pink stroke), @match or @include, and @run-at
Each userscript must include a base64-encoded SVG data URI icon in the @icon metadata field using a simple line-style icon with hot pink (#FF1493) stroke color, placed after @author and before @match
Use @match patterns with the narrowest practical scope (least privilege principle), preferring Chrome-style match patterns like *://example.com/* over broad patterns like *://*/*
Declare all GM APIs used in the @grant metadata field, including legacy (GM_getValue) or async versions (GM.getValue), and avoid implicit grants
For any cross-origin network requests using GM_xmlhttpRequest, declare required domains in @connect metadata entries; avoid @connect * unless truly necessary
Wrap script logic in a single top-level IIFE with 'use strict' directive and provide a clear main() entrypoint
Increment @version for every functional change using datetime format YYYY.MM.DD.HHMM (UTC), ensuring versions never decrease
Ensure DOM updates are idempotent (safe to run multiple times) by checking if initialization has already occurred, marking injected elements, and avoiding duplicate listeners
For any @match *://*/* or similarly broad patterns, implement dormant-by-default behavior: register menu commands and UI shell on init, but only run heavy work when Always Run is enabled or user explicitly triggers via UI
Use consistent logging with a single createLogger() helper per script, store logs under `GM_setValue('userscript.logs.<short...

Files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Prioritize Android XBrowser compatibility above all other considerations; test on XBrowser and ensure scripts work with its built-in script manager
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use a tiny compatibility wrapper (GMX pattern) to handle both GM4 async (`GM.getValue`) and legacy (`GM_getValue`) APIs, enabling cross-manager compatibility
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use canonical CDN URLs for shared dependencies: jQuery from googleapis, jQuery UI from googleapis, Readability from jsDelivr, Turndown/GFM from unpkg (UMD builds, not CJS)
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : If hosting scripts with updates, set both `updateURL` (metadata URL) and `downloadURL` (full script URL) to raw GitHub URLs; increment `version` for any functional change

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Increment `version` for every functional change using datetime format `YYYY.MM.DD.HHMM` (UTC), ensuring versions never decrease

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use a tiny compatibility wrapper (GMX pattern) to handle both GM4 async (`GM.getValue`) and legacy (`GM_getValue`) APIs, enabling cross-manager compatibility

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Declare all GM APIs used in the `grant` metadata field, including legacy (`GM_getValue`) or async versions (`GM.getValue`), and avoid implicit grants

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Implement race condition mitigation when 12+ scripts run simultaneously: stagger initialization with `setTimeout(0)`, use short polling intervals with exponential backoff, batch MutationObserver callbacks, and test thoroughly with all scripts installed

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include required metadata keys: `name`, `namespace` (set to `https://github.com/cbkii/userscripts`), `version` (datetime format `YYYY.MM.DD.HHMM`), `description`, `author`, `icon` (base64 SVG data URI with #FF1493 hot pink stroke), `match` or `include`, and `run-at`

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include clear manual test steps in PR/commit messages or `docs/<script>.md` documenting: happy path (expected behavior), negative path (missing elements, excluded pages), SPA navigation, and XBrowser compatibility checks

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use English-only comments and user-facing strings throughout the script

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Prefer `MutationObserver` and event delegation over polling; use `requestAnimationFrame` for UI alignment work; always disconnect observers and clear timers in teardown

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Provide proper teardown logic: disconnect MutationObservers, clear setInterval/setTimeout timers, remove injected DOM elements, and clean up event listeners when disabling the script

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : For any `match *://*/*` or similarly broad patterns, implement dormant-by-default behavior: register menu commands and UI shell on init, but only run heavy work when Always Run is enabled or user explicitly triggers via UI

Applied to files:

  • chatgptmd.user.js
🧬 Code graph analysis (2)
chatgptmd.user.js (2)
pageinfoexport.user.js (6)
  • isMobileBrowser (306-307)
  • DOWNLOAD_ANCHOR_DELAY_MS (302-302)
  • MOBILE_FALLBACK_DELAY_MS (303-303)
  • BLOB_STALE_MS (304-304)
  • MAX_DATA_URL_FILE_SIZE_BYTES (61-61)
  • blobToDataUrl (926-931)
pagemd.user.js (5)
  • isMobileBrowser (452-453)
  • DOWNLOAD_ANCHOR_DELAY_MS (454-454)
  • MOBILE_FALLBACK_DELAY_MS (455-455)
  • BLOB_STALE_MS (456-456)
  • MAX_DATA_URL_FILE_SIZE_BYTES (64-64)
pageinfoexport.user.js (2)
chatgptmd.user.js (6)
  • MOBILE_FALLBACK_DELAY_MS (76-76)
  • BLOB_STALE_MS (77-77)
  • BLOB_REVOKE_MS (78-78)
  • isMobileBrowser (73-74)
  • DOWNLOAD_ANCHOR_DELAY_MS (75-75)
  • MAX_DATA_URL_FILE_SIZE_BYTES (79-79)
pagemd.user.js (6)
  • MOBILE_FALLBACK_DELAY_MS (455-455)
  • BLOB_STALE_MS (456-456)
  • BLOB_REVOKE_MS (457-457)
  • isMobileBrowser (452-453)
  • DOWNLOAD_ANCHOR_DELAY_MS (454-454)
  • MAX_DATA_URL_FILE_SIZE_BYTES (64-64)
🔇 Additional comments (10)
chatgptmd.user.js (5)

4-4: LGTM: Version bump follows required format.

The version update to 2026.01.10.0913 correctly follows the datetime format requirement from coding guidelines.


73-79: LGTM: Mobile detection and constants are well-defined.

The mobile browser detection pattern is comprehensive, and the timeout/size constants are reasonable for mobile compatibility. The 2MB data URL limit balances functionality with browser memory constraints.


1426-1437: LGTM: Robust blob-to-data-URL conversion helper.

The implementation properly wraps FileReader in a Promise with comprehensive error handling, validating that the result is both a string and non-empty before resolving.


1439-1463: LGTM: Well-structured mobile data URL conversion with proper fallbacks.

The conditional blob-to-data-URL conversion for mobile includes:

  • Size check to avoid exceeding browser limits
  • Proper error handling with fallback to blob URL
  • Debug logging for troubleshooting

The nested try-catch correctly handles both blob retrieval and conversion failures.


1464-1531: LGTM: Mobile fallback coordination is correctly implemented.

The mobile-specific download flow properly implements timeout-based fallback to avoid blocking on potentially hanging GM_download calls:

  • Mobile path: Sets timeout, fires promise without awaiting, returns immediately. Success/failure handled asynchronously via onload/onerror/timeout.
  • Non-mobile path: Awaits promise completion, blocks until download succeeds or fails.

The fallback coordination (fallbackTriggered, clearFallbackTimer, triggerFallback) prevents duplicate fallback invocations and ensures resources are cleaned up correctly in all code paths.

Based on learnings: polling fallback pattern for XBrowser/mobile compatibility.

pageinfoexport.user.js (2)

6-6: LGTM: Version and mobile constants are consistent.

The version bump and mobile detection constants align with the other scripts in this PR, ensuring consistent behaviour across all three files.

Also applies to: 61-61, 303-303, 306-307


962-1111: LGTM: Complex but correct mobile-aware download coordination.

The implementation uses separate resolveLegacy and resolveMobileFallback promises to track download completion across multiple paths:

  • Mobile with promise: Sets timeout, fires without awaiting, waits for mobile fallback
  • Non-mobile with promise: Awaits promise completion synchronously
  • Legacy paths: Use callback-based completion tracking

All code paths properly resolve promises, clear timers, and clean up resources. The complexity is justified by the requirement to support both blocking (non-mobile) and non-blocking (mobile with timeout fallback) download flows.

Based on learnings: polling fallback pattern for XBrowser/mobile compatibility.

pagemd.user.js (3)

4-4: LGTM: Version and mobile constants are consistent across all scripts.

The version bump and mobile detection logic match the implementation in chatgptmd.user.js and pageinfoexport.user.js, ensuring uniform mobile compatibility handling.

Also applies to: 64-64, 452-457


591-623: LGTM: Data URL conversion follows established pattern.

The blob-to-data-URL conversion logic is consistent with the other two scripts, properly handling size checks, error fallback, and debug logging.


624-688: LGTM: Mobile fallback implementation is consistent and correct.

The downloadViaGM function follows the same pattern as chatgptmd.user.js:

  • Mobile path uses timeout-based fallback without awaiting the download promise
  • Non-mobile path awaits promise completion
  • Fallback coordination prevents duplicate invocations
  • Resources are properly cleaned up in all code paths

The implementation is slightly simpler than pageinfoexport.user.js (single promise chain vs. dual promise coordination) but functionally equivalent for this use case.

Based on learnings: polling fallback pattern for XBrowser/mobile compatibility.


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.

@qodo-code-review

qodo-code-review Bot commented Jan 10, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Invalid URL fallback: blobToDataUrl can resolve to an empty string (non-string FileReader.result), and the code
may then pass this empty URL to GM_download without validating or immediately triggering
the existing fallback chain.

Referred Code
const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
  const reader = new FileReader();
  reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
  reader.onerror = () => reject(new Error('Failed to read blob as data URL'));
  reader.readAsDataURL(blob);
});

async function mobileDownload(content, filename, mimeType = 'text/plain') {
  const resource = createDownloadResource(content, `${mimeType};charset=utf-8`);
  const fallback = () => {
    if (!anchorDownload(resource, filename)) {
      dataUrlDownload(resource, filename);
    }
  };

  try {
    if (gmDownloadLegacy || gmDownloadAsync) {
      const cleanupDelay = DOWNLOAD_ANCHOR_DELAY_MS;
      let downloadUrl = resource.getUrl();
      if (isMobileBrowser) {
        try {


 ... (clipped 21 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-review Bot commented Jan 10, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to a011a82

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent mobile download hangs
Suggestion Impact:The commit implements the suggested fixes: it clears the mobile fallback timer in onload/onerror and in the promise then/catch paths, changes the timeout callback to be non-async (no await), adds a fallbackTriggered guard in the timeout, and nulls the timer via a helper to avoid race/hang conditions.

code diff:

@@ -1013,12 +1013,21 @@
       // Use blob URL if any error occurs
     }
 
+    let fallbackTimerId = null;
+    const clearFallbackTimer = () => {
+      if (fallbackTimerId) {
+        clearTimeout(fallbackTimerId);
+        fallbackTimerId = null;
+      }
+    };
+
     const downloadDetails = {
       url: downloadUrl,
       name: filename,
       saveAs: false,
       ...(isMobileBrowser ? { confirm: false } : {}),
       onload: () => {
+        clearFallbackTimer();
         if (fallbackTriggered) return;
         if (resolveMobileFallback) {
           resolveMobileFallback({ success: true });
@@ -1031,6 +1040,7 @@
         }
       },
       onerror: (err) => {
+        clearFallbackTimer();
         if (resolveMobileFallback) {
           resolveMobileFallback({ success: false });
           resolveMobileFallback = null;
@@ -1041,36 +1051,32 @@
 
     try {
       const downloadPromise = GMX.download(downloadDetails);
-      let fallbackTimerId = null;
       if (isMobileBrowser) {
-        fallbackTimerId = setTimeout(async () => {
-          await handleError(new Error('Mobile GM_download timeout'));
+        fallbackTimerId = setTimeout(() => {
+          if (fallbackTriggered) return;
           if (resolveMobileFallback) {
             resolveMobileFallback({ success: false });
             resolveMobileFallback = null;
           }
+          void handleError(new Error('Mobile GM_download timeout'));
         }, MOBILE_FALLBACK_DELAY_MS);
       }
       if (downloadPromise && typeof downloadPromise.then === 'function') {
         const promise = downloadPromise.then(() => {
-          if (fallbackTimerId) {
-            clearTimeout(fallbackTimerId);
-          }
+          clearFallbackTimer();
           if (fallbackTriggered) return;
           resource.cleanup(cleanupDelay);
           if (resolveMobileFallback) {
             resolveMobileFallback({ success: true });
             resolveMobileFallback = null;
           }
-        }).catch(async (err) => {
-          if (fallbackTimerId) {
-            clearTimeout(fallbackTimerId);
-          }
+        }).catch((err) => {
+          clearFallbackTimer();
           if (resolveMobileFallback) {
             resolveMobileFallback({ success: false });
             resolveMobileFallback = null;
           }
-          await handleError(err);
+          return handleError(err);
         });
         if (!isMobileBrowser) {
           await promise;
@@ -1085,9 +1091,7 @@
       }
       if (mobileFallback) {
         await mobileFallback;
-        if (fallbackTimerId) {
-          clearTimeout(fallbackTimerId);
-        }
+        clearFallbackTimer();
       }

Fix a potential download hang and a race condition in the mobile fallback logic
by adjusting async operations in the timeout and clearing the timer in
onload/onerror handlers.

pageinfoexport.user.js [1016-1094]

 const downloadDetails = {
   url: downloadUrl,
   name: filename,
   saveAs: false,
   ...(isMobileBrowser ? { confirm: false } : {}),
   onload: () => {
+    if (fallbackTimerId) {
+      clearTimeout(fallbackTimerId);
+      fallbackTimerId = null;
+    }
     if (fallbackTriggered) return;
     if (resolveMobileFallback) {
       resolveMobileFallback({ success: true });
       resolveMobileFallback = null;
     }
     resource.cleanup(cleanupDelay);
     if (resolveLegacy) {
       resolveLegacy({ success: true });
       resolveLegacy = null;
     }
   },
   onerror: (err) => {
+    if (fallbackTimerId) {
+      clearTimeout(fallbackTimerId);
+      fallbackTimerId = null;
+    }
     if (resolveMobileFallback) {
       resolveMobileFallback({ success: false });
       resolveMobileFallback = null;
     }
     void handleError(err);
   },
 };
 
 try {
   const downloadPromise = GMX.download(downloadDetails);
   let fallbackTimerId = null;
   if (isMobileBrowser) {
-    fallbackTimerId = setTimeout(async () => {
-      await handleError(new Error('Mobile GM_download timeout'));
+    fallbackTimerId = setTimeout(() => {
+      if (fallbackTriggered) return;
       if (resolveMobileFallback) {
         resolveMobileFallback({ success: false });
         resolveMobileFallback = null;
       }
+      void handleError(new Error('Mobile GM_download timeout'));
     }, MOBILE_FALLBACK_DELAY_MS);
   }
   if (downloadPromise && typeof downloadPromise.then === 'function') {
     const promise = downloadPromise.then(() => {
       if (fallbackTimerId) {
         clearTimeout(fallbackTimerId);
+        fallbackTimerId = null;
       }
       if (fallbackTriggered) return;
       resource.cleanup(cleanupDelay);
       if (resolveMobileFallback) {
         resolveMobileFallback({ success: true });
         resolveMobileFallback = null;
       }
-    }).catch(async (err) => {
+    }).catch((err) => {
       if (fallbackTimerId) {
         clearTimeout(fallbackTimerId);
+        fallbackTimerId = null;
       }
       if (resolveMobileFallback) {
         resolveMobileFallback({ success: false });
         resolveMobileFallback = null;
       }
-      await handleError(err);
+      return handleError(err);
     });
     if (!isMobileBrowser) {
       await promise;
     }
     resolveLegacy = null;
   } else if (!isMobileBrowser) {
     await legacyCompletion;
     resolveLegacy = null;
     if (!fallbackTriggered) {
       resource.cleanup(cleanupDelay);
     }
   }
   if (mobileFallback) {
     await mobileFallback;
     if (fallbackTimerId) {
       clearTimeout(fallbackTimerId);
+      fallbackTimerId = null;
     }
   }
 } catch (err) {
   await handleError(err);
   return {

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies and fixes two subtle but critical bugs: a potential hang on mobile due to incorrect await usage in a timeout, and a race condition where a fallback could be triggered incorrectly. These fixes are crucial for the reliability of the download functionality on mobile devices.

High
Reject invalid data URL results
Suggestion Impact:The blobToDataUrl reader.onload handler was changed to validate reader.result and reject with an "Invalid data URL result" error when it is not a non-empty string, matching the suggested robustness improvement.

code diff:

   const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
     const reader = new FileReader();
-    reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
+    reader.onload = () => {
+      if (typeof reader.result !== 'string' || !reader.result) {
+        reject(new Error('Invalid data URL result'));
+        return;
+      }
+      resolve(reader.result);
+    };
     reader.onerror = () => reject(new Error('Failed to read blob as data URL'));
     reader.readAsDataURL(blob);

In blobToDataUrl, reject the promise if FileReader.result is not a valid string
to prevent silent download failures and ensure fallback mechanisms are triggered
correctly.

chatgptmd.user.js [1426-1431]

 const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
   const reader = new FileReader();
-  reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
+  reader.onload = () => {
+    if (typeof reader.result !== 'string' || !reader.result) {
+      reject(new Error('Invalid data URL result'));
+      return;
+    }
+    resolve(reader.result);
+  };
   reader.onerror = () => reject(new Error('Failed to read blob as data URL'));
   reader.readAsDataURL(blob);
 });

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that a non-string reader.result would lead to an empty data URL, causing a silent download failure. By rejecting the promise instead, it ensures proper error handling and allows the application to use its fallback mechanism, which improves the robustness of the download feature.

Medium
Incremental [*]
Contain fallback execution failures

In triggerFallback, wrap the call to fallback(err) in a try...catch block and
Promise.resolve to contain any potential errors and prevent unhandled promise
rejections.

pagemd.user.js [633-639]

 const triggerFallback = (err) => {
   if (fallbackTriggered) return;
   fallbackTriggered = true;
   clearFallbackTimer();
   resource.markStale();
-  fallback(err);
+  try {
+    void Promise.resolve(fallback(err));
+  } catch (_) {
+    // ignore fallback errors to avoid breaking download flow
+  }
 };
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion improves robustness by preventing unhandled exceptions within the fallback mechanism, which is a good practice, though the likelihood and impact of such an error are moderate.

Low
  • Update

Previous suggestions

✅ Suggestions up to commit b6eb1e4
CategorySuggestion                                                                                                                                    Impact
High-level
Consolidate duplicated mobile download logic
Suggestion Impact:The commit does not actually consolidate the logic into a shared reusable module/function across the scripts, but it does refactor and standardize the duplicated mobile download/GM_download fallback handling in both chatgptmd.user.js and pagemd.user.js (e.g., introducing a single triggerFallback path, guarding against double-fallback, and aligning cleanup delay behavior). This reduces divergence and maintenance risk in the duplicated logic, partially addressing the concern without full consolidation.

code diff:

@@ -1425,7 +1425,13 @@
 
   const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
     const reader = new FileReader();
-    reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
+    reader.onload = () => {
+      if (typeof reader.result !== 'string' || !reader.result) {
+        reject(new Error('Invalid data URL result'));
+        return;
+      }
+      resolve(reader.result);
+    };
     reader.onerror = () => reject(new Error('Failed to read blob as data URL'));
     reader.readAsDataURL(blob);
   });
@@ -1440,7 +1446,7 @@
 
     try {
       if (gmDownloadLegacy || gmDownloadAsync) {
-        const cleanupDelay = DOWNLOAD_ANCHOR_DELAY_MS;
+        const cleanupDelay = gmDownloadAsync ? DOWNLOAD_ANCHOR_DELAY_MS : BLOB_REVOKE_MS;
         let downloadUrl = resource.getUrl();
         if (isMobileBrowser) {
           try {
@@ -1456,11 +1462,19 @@
           }
         }
         let fallbackTimerId = null;
+        let fallbackTriggered = false;
         const clearFallbackTimer = () => {
           if (fallbackTimerId) {
             clearTimeout(fallbackTimerId);
             fallbackTimerId = null;
           }
+        };
+        const triggerFallback = () => {
+          if (fallbackTriggered) return;
+          fallbackTriggered = true;
+          clearFallbackTimer();
+          resource.markStale();
+          fallback();
         };
         const detail = {
           url: downloadUrl,
@@ -1468,36 +1482,39 @@
           saveAs: true,
           ...(isMobileBrowser ? { confirm: false } : {}),
           onload: () => {
+            if (fallbackTriggered) return;
             clearFallbackTimer();
             resource.cleanup(cleanupDelay);
           },
           onerror: () => {
-            clearFallbackTimer();
-            resource.markStale();
-            fallback();
+            triggerFallback();
           }
         };
         const result = gmDownloadAsync ? gmDownloadAsync(detail) : gmDownloadLegacy(detail);
         if (isMobileBrowser) {
           fallbackTimerId = setTimeout(() => {
-            resource.markStale();
-            fallback();
+            triggerFallback();
           }, MOBILE_FALLBACK_DELAY_MS);
         }
         if (result && typeof result.then === 'function') {
           const promise = result.then(() => {
+            if (fallbackTriggered) return;
             clearFallbackTimer();
             resource.cleanup(cleanupDelay);
           }).catch(() => {
-            clearFallbackTimer();
-            resource.markStale();
-            fallback();
+            triggerFallback();
           });
           if (!isMobileBrowser) {
             await promise;
           }
         } else {
-          setTimeout(() => resource.cleanup(cleanupDelay), cleanupDelay);
+          if (!fallbackTriggered) {
+            setTimeout(() => {
+              if (!fallbackTriggered) {
+                resource.cleanup(cleanupDelay);
+              }
+            }, cleanupDelay);
+          }
         }
         return true;
       }

# File: pagemd.user.js
@@ -1,7 +1,7 @@
 // ==UserScript==
 // @name         Easy Web Page to Markdown
 // @namespace    https://github.com/cbkii/userscripts
-// @version      2026.01.10.0804
+// @version      2026.01.10.0842
 // @description  Extracts the main article content and saves it as clean Markdown with a single click.
 // @author       cbkii
 // @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkYxNDkzIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTE0IDJINmEyIDIgMCAwIDAtMiAydjE2YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY4eiIvPjxwb2x5bGluZSBwb2ludHM9IjE0IDIgMTQgOCAyMCA4Ii8+PHBhdGggZD0iTTEwIDEzaDQiLz48cGF0aCBkPSJNMTAgMTdoNCIvPjxwYXRoIGQ9Ik0xMCA5aDIiLz48L3N2Zz4=
@@ -621,12 +621,21 @@
       // Use blob URL if any error occurs
     }
     
+    const cleanupDelay = gmDownloadAsync ? DOWNLOAD_ANCHOR_DELAY_MS : BLOB_REVOKE_MS;
     let fallbackTimerId = null;
+    let fallbackTriggered = false;
     const clearFallbackTimer = () => {
       if (fallbackTimerId) {
         clearTimeout(fallbackTimerId);
         fallbackTimerId = null;
       }
+    };
+    const triggerFallback = (err) => {
+      if (fallbackTriggered) return;
+      fallbackTriggered = true;
+      clearFallbackTimer();
+      resource.markStale();
+      fallback(err);
     };
 
     const detail = {
@@ -635,37 +644,40 @@
       saveAs: false,
       ...(isMobileBrowser ? { confirm: false } : {}),
       onload: () => {
+        if (fallbackTriggered) return;
         clearFallbackTimer();
-        resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS);
+        resource.cleanup(cleanupDelay);
       },
       onerror: (err) => {
-        clearFallbackTimer();
-        resource.markStale();
-        fallback(err);
+        triggerFallback(err);
       },
     };
     try {
       const result = gmDownloadAsync ? gmDownloadAsync(detail) : gmDownloadLegacy(detail);
       if (isMobileBrowser) {
         fallbackTimerId = setTimeout(() => {
-          resource.markStale();
-          fallback(new Error('Mobile GM_download timeout'));
+          triggerFallback(new Error('Mobile GM_download timeout'));
         }, MOBILE_FALLBACK_DELAY_MS);
       }
       if (result && typeof result.then === 'function') {
         const promise = result.then(() => {
+          if (fallbackTriggered) return;
           clearFallbackTimer();
-          resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS);
+          resource.cleanup(cleanupDelay);
         }).catch((err) => {
-          clearFallbackTimer();
-          resource.markStale();
-          fallback(err);
+          triggerFallback(err);
         });
         if (!isMobileBrowser) {
           await promise;
         }
       } else {
-        setTimeout(() => resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS), DOWNLOAD_ANCHOR_DELAY_MS);
+        if (!fallbackTriggered) {
+          setTimeout(() => {
+            if (!fallbackTriggered) {
+              resource.cleanup(cleanupDelay);
+            }
+          }, cleanupDelay);
+        }
       }
       return true;
     } catch (err) {

The mobile download fallback logic, including timeout handling and data URL
conversion, is duplicated across three userscripts. This should be consolidated
into a reusable pattern to reduce maintenance overhead.

Examples:

chatgptmd.user.js [1433-1502]
  async function mobileDownload(content, filename, mimeType = 'text/plain') {
    const resource = createDownloadResource(content, `${mimeType};charset=utf-8`);
    const fallback = () => {
      if (!anchorDownload(resource, filename)) {
        dataUrlDownload(resource, filename);
      }
    };

    try {
      if (gmDownloadLegacy || gmDownloadAsync) {

 ... (clipped 60 lines)
pagemd.user.js [591-676]
  const downloadViaGM = async (resource, filename, fallback) => {
    if (!gmDownloadLegacy && !gmDownloadAsync) return false;
    
    // Mobile compatibility: prefer data URL for better download manager support
    // Blob URLs may fail silently on some Android browsers
    let downloadUrl = resource.getUrl();
    try {
      const blob = resource.getBlob();
      // Only convert to data URL if size is reasonable (< 2MB for mobile compatibility)
      if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {

 ... (clipped 76 lines)

Solution Walkthrough:

Before:

// In chatgptmd.user.js, pagemd.user.js, and pageinfoexport.user.js

const isMobileBrowser = /Android|iPhone|.../.test(navigator.userAgent);
const MOBILE_FALLBACK_DELAY_MS = 1200;

async function downloadFunction(...) {
  let downloadUrl = resource.getUrl();
  if (isMobileBrowser && blob.size < MAX_SIZE) {
    downloadUrl = await blobToDataUrl(blob);
  }

  let fallbackTimerId = null;
  const detail = {
    url: downloadUrl,
    ...(isMobileBrowser ? { confirm: false } : {}),
    // ... onload/onerror handlers that clear timer
  };

  const promise = GM.download(detail);
  if (isMobileBrowser) {
    fallbackTimerId = setTimeout(fallback, MOBILE_FALLBACK_DELAY_MS);
  } else {
    await promise;
  }
}

After:

// Shared utility module (conceptual)
function createMobileDownloader(GM_download) {
  const isMobileBrowser = /Android|iPhone|.../.test(navigator.userAgent);

  return async function mobileDownload(resource, filename, fallback) {
    // ... consolidated logic for data URL conversion, timeout, and fallbacks
    // ... handles both mobile and non-mobile cases internally
  };
}

// In chatgptmd.user.js (and other scripts)
// import { createMobileDownloader } from './shared-download.js'; // (if modules were possible)

// Since modules are not used, this logic would be in a shared library
// or a more abstract function within each script, but defined only once.
const downloadHandler = createMobileDownloader(GM.download);

async function downloadFunction(...) {
  await downloadHandler(resource, filename, fallback);
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies significant duplication of complex, asynchronous download logic across three files, which introduces a major long-term maintenance risk.

High
Possible issue
Prevent premature blob URL revocation
Suggestion Impact:The commit changes cleanupDelay from always DOWNLOAD_ANCHOR_DELAY_MS to a conditional that uses BLOB_REVOKE_MS for legacy GM_download and DOWNLOAD_ANCHOR_DELAY_MS for gmDownloadAsync, matching the intent to prevent premature blob URL revocation. Additional fallback-trigger guarding logic was also added but is ancillary to the suggestion.

code diff:

@@ -1440,7 +1440,7 @@
 
     try {
       if (gmDownloadLegacy || gmDownloadAsync) {
-        const cleanupDelay = DOWNLOAD_ANCHOR_DELAY_MS;
+        const cleanupDelay = gmDownloadAsync ? DOWNLOAD_ANCHOR_DELAY_MS : BLOB_REVOKE_MS;
         let downloadUrl = resource.getUrl();

Restore the conditional logic for cleanupDelay to use a longer delay for legacy
GM_download to prevent premature blob URL revocation and potential download
failures.

chatgptmd.user.js [1443]

-const cleanupDelay = DOWNLOAD_ANCHOR_DELAY_MS;
+const cleanupDelay = gmDownloadAsync ? DOWNLOAD_ANCHOR_DELAY_MS : BLOB_REVOKE_MS;

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a regression that could cause downloads to fail for users with legacy script managers by prematurely revoking the blob URL.

Medium
Prevent premature resource cleanup on downloads
Suggestion Impact:The commit introduces a unified `cleanupDelay` that uses `BLOB_REVOKE_MS` when `gmDownloadLegacy` is used (and `DOWNLOAD_ANCHOR_DELAY_MS` for async GM_download), and applies this delay to all cleanup paths including the legacy fallback timer. It also adds a `fallbackTriggered` guard to prevent cleanup/fallback races.

code diff:

+    const cleanupDelay = gmDownloadAsync ? DOWNLOAD_ANCHOR_DELAY_MS : BLOB_REVOKE_MS;
     let fallbackTimerId = null;
+    let fallbackTriggered = false;
     const clearFallbackTimer = () => {
       if (fallbackTimerId) {
         clearTimeout(fallbackTimerId);
         fallbackTimerId = null;
       }
+    };
+    const triggerFallback = (err) => {
+      if (fallbackTriggered) return;
+      fallbackTriggered = true;
+      clearFallbackTimer();
+      resource.markStale();
+      fallback(err);
     };
 
     const detail = {
@@ -635,37 +644,40 @@
       saveAs: false,
       ...(isMobileBrowser ? { confirm: false } : {}),
       onload: () => {
+        if (fallbackTriggered) return;
         clearFallbackTimer();
-        resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS);
+        resource.cleanup(cleanupDelay);
       },
       onerror: (err) => {
-        clearFallbackTimer();
-        resource.markStale();
-        fallback(err);
+        triggerFallback(err);
       },
     };
     try {
       const result = gmDownloadAsync ? gmDownloadAsync(detail) : gmDownloadLegacy(detail);
       if (isMobileBrowser) {
         fallbackTimerId = setTimeout(() => {
-          resource.markStale();
-          fallback(new Error('Mobile GM_download timeout'));
+          triggerFallback(new Error('Mobile GM_download timeout'));
         }, MOBILE_FALLBACK_DELAY_MS);
       }
       if (result && typeof result.then === 'function') {
         const promise = result.then(() => {
+          if (fallbackTriggered) return;
           clearFallbackTimer();
-          resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS);
+          resource.cleanup(cleanupDelay);
         }).catch((err) => {
-          clearFallbackTimer();
-          resource.markStale();
-          fallback(err);
+          triggerFallback(err);
         });
         if (!isMobileBrowser) {
           await promise;
         }
       } else {
-        setTimeout(() => resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS), DOWNLOAD_ANCHOR_DELAY_MS);
+        if (!fallbackTriggered) {
+          setTimeout(() => {
+            if (!fallbackTriggered) {
+              resource.cleanup(cleanupDelay);
+            }
+          }, cleanupDelay);
+        }
       }

Increase the resource cleanup delay for legacy GM_download calls to prevent
premature blob URL revocation and potential download failures.

pagemd.user.js [648-669]

 const result = gmDownloadAsync ? gmDownloadAsync(detail) : gmDownloadLegacy(detail);
 if (isMobileBrowser) {
   fallbackTimerId = setTimeout(() => {
     resource.markStale();
     fallback(new Error('Mobile GM_download timeout'));
   }, MOBILE_FALLBACK_DELAY_MS);
 }
 if (result && typeof result.then === 'function') {
   const promise = result.then(() => {
     clearFallbackTimer();
     resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS);
   }).catch((err) => {
     clearFallbackTimer();
     resource.markStale();
     fallback(err);
   });
   if (!isMobileBrowser) {
     await promise;
   }
 } else {
-  setTimeout(() => resource.cleanup(DOWNLOAD_ANCHOR_DELAY_MS), DOWNLOAD_ANCHOR_DELAY_MS);
+  // For legacy GM_download, use a long delay before cleanup
+  setTimeout(() => resource.cleanup(BLOB_REVOKE_MS), BLOB_REVOKE_MS);
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that using a short 500ms cleanup delay for legacy downloads is a regression that could cause downloads to fail.

Medium

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @chatgptmd.user.js:
- Around line 1481-1501: There’s a race where the mobile fallback timer and the
promise handlers can both call fallback(), causing duplicate downloads; add a
single-run guard (e.g., a boolean like fallbackInvoked or a method on resource
such as resource.fallbackInvoked) and check/set it whenever you would call
fallback() (in the timeout, in the promise.catch, and anywhere else that
triggers fallback), ensure clearFallbackTimer and the promise resolution path
respect the guard, and only call resource.markStale(),
resource.cleanup(cleanupDelay) or fallback() when the guard indicates the action
hasn’t already run.
🧹 Nitpick comments (2)
pagemd.user.js (1)

594-622: Consider limiting data URL conversion to mobile browsers only.

The data URL conversion is performed unconditionally for all files under 2MB, but the comment states it's for "Mobile compatibility". On desktop, blob URLs work reliably and are more memory-efficient. Consider gating this conversion behind isMobileBrowser to avoid unnecessary processing on desktop:

♻️ Suggested optimisation
     // Mobile compatibility: prefer data URL for better download manager support
     // Blob URLs may fail silently on some Android browsers
     let downloadUrl = resource.getUrl();
-    try {
-      const blob = resource.getBlob();
-      // Only convert to data URL if size is reasonable (< 2MB for mobile compatibility)
-      if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
+    if (isMobileBrowser) {
+      try {
+        const blob = resource.getBlob();
+        // Only convert to data URL if size is reasonable (< 2MB for mobile compatibility)
+        if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
+          const reader = new FileReader();
+          const dataUrlPromise = new Promise((resolve, reject) => {
+            reader.onload = () => resolve(reader.result);
+            reader.onerror = () => reject(new Error('Failed to read blob'));
+            reader.readAsDataURL(blob);
+          });
+          try {
+            downloadUrl = await dataUrlPromise;
+          } catch (err) {
+            if (DEBUG) {
+              logWarn('Failed to convert blob to data URL, falling back to blob URL', { error: err?.message || String(err) });
+            }
+            downloadUrl = resource.getUrl();
+          }
+        }
+      } catch (err) {
+        if (DEBUG) {
+          logWarn('Error while preparing download URL, using original blob URL', { error: err?.message || String(err) });
+        }
+      }
+    }
pageinfoexport.user.js (1)

985-1009: Same suggestion as pagemd.user.js: consider mobile-only data URL conversion.

This data URL conversion logic is identical to pagemd.user.js. The same optional refactor applies: gating behind isMobileBrowser would avoid unnecessary processing on desktop browsers where blob URLs work reliably.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0e93cc and b6eb1e4.

📒 Files selected for processing (3)
  • chatgptmd.user.js
  • pageinfoexport.user.js
  • pagemd.user.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.user.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.user.js: Userscript metadata block must be positioned at the very beginning of the file with exact syntax: // ==UserScript== and // ==/UserScript== on their own lines, each metadata line starting with // (exactly one space after //)
Include required metadata keys: @name, @namespace (set to https://github.com/cbkii/userscripts), @version (datetime format YYYY.MM.DD.HHMM), @description, @author, @icon (base64 SVG data URI with #FF1493 hot pink stroke), @match or @include, and @run-at
Each userscript must include a base64-encoded SVG data URI icon in the @icon metadata field using a simple line-style icon with hot pink (#FF1493) stroke color, placed after @author and before @match
Use @match patterns with the narrowest practical scope (least privilege principle), preferring Chrome-style match patterns like *://example.com/* over broad patterns like *://*/*
Declare all GM APIs used in the @grant metadata field, including legacy (GM_getValue) or async versions (GM.getValue), and avoid implicit grants
For any cross-origin network requests using GM_xmlhttpRequest, declare required domains in @connect metadata entries; avoid @connect * unless truly necessary
Wrap script logic in a single top-level IIFE with 'use strict' directive and provide a clear main() entrypoint
Increment @version for every functional change using datetime format YYYY.MM.DD.HHMM (UTC), ensuring versions never decrease
Ensure DOM updates are idempotent (safe to run multiple times) by checking if initialization has already occurred, marking injected elements, and avoiding duplicate listeners
For any @match *://*/* or similarly broad patterns, implement dormant-by-default behavior: register menu commands and UI shell on init, but only run heavy work when Always Run is enabled or user explicitly triggers via UI
Use consistent logging with a single createLogger() helper per script, store logs under `GM_setValue('userscript.logs.<short...

Files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include clear manual test steps in PR/commit messages or `docs/<script>.md` documenting: happy path (expected behavior), negative path (missing elements, excluded pages), SPA navigation, and XBrowser compatibility checks
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Prioritize Android XBrowser compatibility above all other considerations; test on XBrowser and ensure scripts work with its built-in script manager
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use canonical CDN URLs for shared dependencies: jQuery from googleapis, jQuery UI from googleapis, Readability from jsDelivr, Turndown/GFM from unpkg (UMD builds, not CJS)
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use a tiny compatibility wrapper (GMX pattern) to handle both GM4 async (`GM.getValue`) and legacy (`GM_getValue`) APIs, enabling cross-manager compatibility

Applied to files:

  • pagemd.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Declare all GM APIs used in the `grant` metadata field, including legacy (`GM_getValue`) or async versions (`GM.getValue`), and avoid implicit grants

Applied to files:

  • pagemd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include required metadata keys: `name`, `namespace` (set to `https://github.com/cbkii/userscripts`), `version` (datetime format `YYYY.MM.DD.HHMM`), `description`, `author`, `icon` (base64 SVG data URI with #FF1493 hot pink stroke), `match` or `include`, and `run-at`

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : If hosting scripts with updates, set both `updateURL` (metadata URL) and `downloadURL` (full script URL) to raw GitHub URLs; increment `version` for any functional change

Applied to files:

  • pageinfoexport.user.js
🧬 Code graph analysis (2)
pagemd.user.js (2)
chatgptmd.user.js (5)
  • isMobileBrowser (73-74)
  • DOWNLOAD_ANCHOR_DELAY_MS (75-75)
  • MOBILE_FALLBACK_DELAY_MS (76-76)
  • gmDownloadAsync (70-72)
  • gmDownloadLegacy (69-69)
pageinfoexport.user.js (3)
  • isMobileBrowser (306-307)
  • DOWNLOAD_ANCHOR_DELAY_MS (302-302)
  • MOBILE_FALLBACK_DELAY_MS (303-303)
chatgptmd.user.js (2)
pageinfoexport.user.js (7)
  • isMobileBrowser (306-307)
  • DOWNLOAD_ANCHOR_DELAY_MS (302-302)
  • MOBILE_FALLBACK_DELAY_MS (303-303)
  • BLOB_STALE_MS (304-304)
  • BLOB_REVOKE_MS (305-305)
  • MAX_DATA_URL_FILE_SIZE_BYTES (61-61)
  • blobToDataUrl (926-931)
pagemd.user.js (6)
  • isMobileBrowser (452-453)
  • DOWNLOAD_ANCHOR_DELAY_MS (454-454)
  • MOBILE_FALLBACK_DELAY_MS (455-455)
  • BLOB_STALE_MS (456-456)
  • BLOB_REVOKE_MS (457-457)
  • MAX_DATA_URL_FILE_SIZE_BYTES (64-64)
🔇 Additional comments (9)
pagemd.user.js (2)

452-455: LGTM! Mobile detection and constants are consistent across the PR.

The isMobileBrowser regex and MOBILE_FALLBACK_DELAY_MS constant align with the other scripts in this PR (pageinfoexport.user.js and chatgptmd.user.js), ensuring consistent mobile behaviour across the userscript suite. Based on learnings, this follows the polling fallback pattern for XBrowser compatibility.


624-630: LGTM! Clean timer cleanup helper.

The clearFallbackTimer helper properly clears and nullifies the timer ID, preventing double-clearing issues. This pattern is consistent with chatgptmd.user.js.

pageinfoexport.user.js (2)

966-975: LGTM! Promise coordination for mobile fallback path.

The separate resolveMobileFallback promise allows the function to wait for mobile completion while still supporting the legacy callback-based path. This is a reasonable approach for coordinating the asynchronous completion signals.


1011-1034: LGTM! Download details properly handle both mobile and non-mobile paths.

The download details correctly:

  • Set confirm: false only on mobile for better download manager compatibility
  • Resolve resolveMobileFallback in both onload and onerror to ensure the mobile promise completes
  • Use void handleError(err) to properly handle the async error handler
chatgptmd.user.js (5)

73-79: LGTM! Mobile constants are consistent with other files.

The mobile detection regex and MOBILE_FALLBACK_DELAY_MS match the other scripts. Using 2 * 1024 * 1024 for MAX_DATA_URL_FILE_SIZE_BYTES is more readable than the literal 2097152 used in the other files—consider updating the others for consistency.


1426-1431: LGTM! Helper is consistent with pageinfoexport.user.js.

The blobToDataUrl helper implementation is identical to pageinfoexport.user.js, ensuring consistent behaviour. Per coding guidelines, each script must be self-contained, so this duplication is acceptable.


1445-1457: Good: Data URL conversion correctly gated to mobile-only here.

Unlike pagemd.user.js and pageinfoexport.user.js, this file correctly gates the data URL conversion behind isMobileBrowser. This is the preferred approach—consider aligning the other two files with this pattern.


1465-1479: Inconsistent saveAs value across files.

This file uses saveAs: true (line 1468), while pagemd.user.js and pageinfoexport.user.js use saveAs: false. This affects whether users see a "Save As" dialog:

  • saveAs: true — prompts user for save location
  • saveAs: false — saves directly to default downloads folder

Verify this inconsistency is intentional. If ChatGPT exports should behave consistently with other scripts, consider aligning the value.


512-513: LGTM! Proper async integration.

The exportChat function now correctly awaits mobileDownload for download actions, ensuring proper async flow.

Comment thread chatgptmd.user.js
Comment thread pageinfoexport.user.js Outdated
Comment thread pagemd.user.js
Manual test:
- On a mobile browser (Android/iOS), export Page → Markdown and confirm the download completes.
- On a mobile browser, export Page Info and confirm the .txt download completes.
- On a mobile browser, export a ChatGPT conversation and confirm the download completes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
pagemd.user.js (1)

594-623: Mobile data-URL conversion currently runs on desktop too (likely unintended). Lines 594-623.

The comment says “Mobile compatibility”, but the conversion runs regardless of isMobileBrowser, changing non-mobile behaviour and adding FileReader work for <2MB exports.

Proposed fix (gate conversion to mobile, ensure string)
-    // Mobile compatibility: prefer data URL for better download manager support
-    // Blob URLs may fail silently on some Android browsers
-    let downloadUrl = resource.getUrl();
-    try {
-      const blob = resource.getBlob();
-      // Only convert to data URL if size is reasonable (< 2MB for mobile compatibility)
-      if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
-        const reader = new FileReader();
-        const dataUrlPromise = new Promise((resolve, reject) => {
-          reader.onload = () => resolve(reader.result);
-          reader.onerror = () => reject(new Error('Failed to read blob'));
-          reader.readAsDataURL(blob);
-        });
-        try {
-          downloadUrl = await dataUrlPromise;
-        } catch (err) {
-          if (DEBUG) {
-            logWarn('Failed to convert blob to data URL, falling back to blob URL', { error: err?.message || String(err) });
-          }
-          // Fall back to blob URL if data URL conversion fails
-          downloadUrl = resource.getUrl();
-        }
-      }
-    } catch (err) {
-      if (DEBUG) {
-        logWarn('Error while preparing download URL, using original blob URL', { error: err?.message || String(err) });
-      }
-      // Use blob URL if any error occurs
-    }
+    let downloadUrl = resource.getUrl();
+    if (isMobileBrowser) {
+      // Mobile compatibility: prefer data URL for better download manager support
+      // Blob URLs may fail silently on some Android browsers
+      try {
+        const blob = resource.getBlob();
+        if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
+          const reader = new FileReader();
+          downloadUrl = await new Promise((resolve, reject) => {
+            reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
+            reader.onerror = () => reject(new Error('Failed to read blob'));
+            reader.readAsDataURL(blob);
+          });
+          if (!downloadUrl) {
+            downloadUrl = resource.getUrl();
+          }
+        }
+      } catch (err) {
+        if (DEBUG) {
+          logWarn('Failed to convert blob to data URL, falling back to blob URL', { error: err?.message || String(err) });
+        }
+        downloadUrl = resource.getUrl();
+      }
+    }
pageinfoexport.user.js (1)

990-1015: Same issue as pagemd: “mobile” data-URL conversion runs even when not mobile, and duplicates blobToDataUrl. Lines 990-1015.

Proposed fix (gate + reuse helper)
-    // Mobile compatibility: prefer data URL for better download manager support
-    // Blob URLs may fail silently on some Android browsers
     let downloadUrl = resource.getUrl();
-    try {
-      const blob = resource.getBlob();
-      // Only convert to data URL if size is reasonable (< 2MB for mobile compatibility)
-      if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
-        const reader = new FileReader();
-        const dataUrlPromise = new Promise((resolve, reject) => {
-          reader.onload = () => resolve(reader.result);
-          reader.onerror = () => reject(new Error('Failed to read blob'));
-          reader.readAsDataURL(blob);
-        });
-        try {
-          downloadUrl = await dataUrlPromise;
-        } catch (err) {
-          log('warn', 'Failed to convert blob to data URL, falling back to blob URL', { error: err?.message || String(err) });
-          // Fall back to blob URL if data URL conversion fails
-          downloadUrl = resource.getUrl();
-        }
-      }
-    } catch (err) {
-      log('warn', 'Error while preparing download URL, using original blob URL', { error: err?.message || String(err) });
-      // Use blob URL if any error occurs
-    }
+    if (isMobileBrowser) {
+      // Mobile compatibility: prefer data URL for better download manager support
+      // Blob URLs may fail silently on some Android browsers
+      try {
+        const blob = resource.getBlob();
+        if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
+          const dataUrl = await blobToDataUrl(blob);
+          if (dataUrl) {
+            downloadUrl = dataUrl;
+          }
+        }
+      } catch (err) {
+        log('warn', 'Failed to convert blob to data URL, falling back to blob URL', { error: err?.message || String(err) });
+        downloadUrl = resource.getUrl();
+      }
+    }
🤖 Fix all issues with AI agents
In @chatgptmd.user.js:
- Around line 1444-1512: The code passes an undocumented Tampermonkey
GM_download option confirm: false for mobile (see the detail object creation and
use in gmDownloadAsync/gmDownloadLegacy), which may be ignored or break on
non‑XBrowser managers; remove the confirm: false field or gate it behind
explicit XBrowser detection (add an isXBrowser flag and only include { confirm:
false } when true) and keep relying on the existing MOBILE_FALLBACK_DELAY_MS
fallback logic; also ensure MOBILE_FALLBACK_DELAY_MS has been validated for
Android/iOS and adjust it if real‑world testing shows 1200ms is too aggressive.

In @pageinfoexport.user.js:
- Around line 1016-1040: The onload and onerror callbacks inside the
downloadDetails object should defensively clear fallbackTimerId to avoid the
fallback timeout firing after the download finishes; at the start of
downloadDetails.onload and downloadDetails.onerror add a check like "if
(fallbackTimerId) clearTimeout(fallbackTimerId)" before calling
resolveMobileFallback/resolveLegacy, resource.cleanup, or handleError so the
timer is always cancelled even if the promise handlers (.then/.catch) haven't
run yet.
🧹 Nitpick comments (4)
pagemd.user.js (1)

4-4: Version bump looks fine; consider adding @sandbox DOM to align with repo rules. Line 4.

This change is OK. Noting the metadata block still lacks @sandbox DOM (required by your guidelines for DOM-only scripts).

pageinfoexport.user.js (2)

6-6: Version bump OK; metadata still missing @license / @sandbox DOM per guidelines. Line 6.


962-988: Good: single-fire fallback via fallbackTriggered; but legacy completion machinery looks redundant now. Lines 962-988.

Because GMX.download() always yields a thenable in this file, resolveLegacy/legacyCompletion can likely be removed to reduce state and edge cases.

chatgptmd.user.js (1)

4-4: Version bump OK; metadata still missing @license / @sandbox DOM per guidelines. Line 4.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6eb1e4 and a011a82.

📒 Files selected for processing (3)
  • chatgptmd.user.js
  • pageinfoexport.user.js
  • pagemd.user.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.user.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.user.js: Userscript metadata block must be positioned at the very beginning of the file with exact syntax: // ==UserScript== and // ==/UserScript== on their own lines, each metadata line starting with // (exactly one space after //)
Include required metadata keys: @name, @namespace (set to https://github.com/cbkii/userscripts), @version (datetime format YYYY.MM.DD.HHMM), @description, @author, @icon (base64 SVG data URI with #FF1493 hot pink stroke), @match or @include, and @run-at
Each userscript must include a base64-encoded SVG data URI icon in the @icon metadata field using a simple line-style icon with hot pink (#FF1493) stroke color, placed after @author and before @match
Use @match patterns with the narrowest practical scope (least privilege principle), preferring Chrome-style match patterns like *://example.com/* over broad patterns like *://*/*
Declare all GM APIs used in the @grant metadata field, including legacy (GM_getValue) or async versions (GM.getValue), and avoid implicit grants
For any cross-origin network requests using GM_xmlhttpRequest, declare required domains in @connect metadata entries; avoid @connect * unless truly necessary
Wrap script logic in a single top-level IIFE with 'use strict' directive and provide a clear main() entrypoint
Increment @version for every functional change using datetime format YYYY.MM.DD.HHMM (UTC), ensuring versions never decrease
Ensure DOM updates are idempotent (safe to run multiple times) by checking if initialization has already occurred, marking injected elements, and avoiding duplicate listeners
For any @match *://*/* or similarly broad patterns, implement dormant-by-default behavior: register menu commands and UI shell on init, but only run heavy work when Always Run is enabled or user explicitly triggers via UI
Use consistent logging with a single createLogger() helper per script, store logs under `GM_setValue('userscript.logs.<short...

Files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include clear manual test steps in PR/commit messages or `docs/<script>.md` documenting: happy path (expected behavior), negative path (missing elements, excluded pages), SPA navigation, and XBrowser compatibility checks
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Prioritize Android XBrowser compatibility above all other considerations; test on XBrowser and ensure scripts work with its built-in script manager
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use canonical CDN URLs for shared dependencies: jQuery from googleapis, jQuery UI from googleapis, Readability from jsDelivr, Turndown/GFM from unpkg (UMD builds, not CJS)
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use a tiny compatibility wrapper (GMX pattern) to handle both GM4 async (`GM.getValue`) and legacy (`GM_getValue`) APIs, enabling cross-manager compatibility

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Declare all GM APIs used in the `grant` metadata field, including legacy (`GM_getValue`) or async versions (`GM.getValue`), and avoid implicit grants

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include required metadata keys: `name`, `namespace` (set to `https://github.com/cbkii/userscripts`), `version` (datetime format `YYYY.MM.DD.HHMM`), `description`, `author`, `icon` (base64 SVG data URI with #FF1493 hot pink stroke), `match` or `include`, and `run-at`

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : If hosting scripts with updates, set both `updateURL` (metadata URL) and `downloadURL` (full script URL) to raw GitHub URLs; increment `version` for any functional change

Applied to files:

  • pageinfoexport.user.js
🧬 Code graph analysis (1)
chatgptmd.user.js (6)
pageinfoexport.user.js (8)
  • isMobileBrowser (306-307)
  • DOWNLOAD_ANCHOR_DELAY_MS (302-302)
  • MOBILE_FALLBACK_DELAY_MS (303-303)
  • BLOB_STALE_MS (304-304)
  • MAX_DATA_URL_FILE_SIZE_BYTES (61-61)
  • blobToDataUrl (926-931)
  • DEBUG (53-53)
  • log (409-414)
pageunlock.user.js (2)
  • DEBUG (59-59)
  • log (61-61)
searchduck.user.js (2)
  • DEBUG (70-70)
  • log (500-505)
antiadblock.user.js (2)
  • DEBUG (61-61)
  • log (315-320)
userscriptlogs.user.js (2)
  • DEBUG (47-47)
  • log (275-280)
searchgoogle.user.js (2)
  • DEBUG (64-64)
  • log (451-456)
🔇 Additional comments (8)
pagemd.user.js (3)

452-456: isMobileBrowser UA sniffing is acceptable, but keep it strictly “mobile-only” logic. Lines 452-456.

Given how broad /Mobile/ can be, it’s important that mobile-only behaviour stays gated behind isMobileBrowser (see download URL conversion below).


624-640: Fallback timer helpers are clean and single-fire. Lines 624-640.


641-654: confirm: false is XBrowser-specific and unsupported in Tampermonkey/Violentmonkey. Lines 641–654.
The confirm option is only documented in XBrowser's GM_download API. Tampermonkey and Violentmonkey do not support this option and will silently ignore it. The conditional application (isMobileBrowser ? { confirm: false } : {}) works correctly but should include a comment clarifying that this targets XBrowser compatibility only, not other mobile managers.

pageinfoexport.user.js (2)

302-307: Mobile detection + fallback delay are consistent with the PR intent. Lines 302-307.


1044-1091: Mobile timeout flow is sensible; please validate no “timeout then success” duplicate on real devices. Lines 1044-1091.

Given the low 1200ms threshold, please test at least one “slow storage / slow manager prompt” case to ensure the timeout doesn’t routinely trigger a second fallback download. Based on learnings, align the manual test pass with the ANDROID-XBROWSER-TESTING.md / ANDROID-XBROWSER-FIXES-TECHNICAL.md guidance.

chatgptmd.user.js (3)

73-80: Constants look coherent; good that the data-URL size limit is explicit. Lines 73-80.


513-514: Good: awaiting the download path avoids “UI says done” before the export attempt finishes. Lines 513-514.


1426-1431: Nice: blobToDataUrl helper keeps the mobile conversion readable. Lines 1426-1431.

Comment thread chatgptmd.user.js
Comment thread pageinfoexport.user.js
Comment thread pagemd.user.js Outdated
Comment on lines 656 to 681

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Tampermonkey GM_download promise resolution timing onload onerror callback

💡 Result:

Short answer: GM_download's Promise (or the async GM.download in newer managers) resolves when the download finishes — i.e., at the same point the onload callback is invoked. The onerror callback rejects/handles failure. In practice you can treat the Promise as equivalent to wiring onload → resolve and onerror → reject. [1][2][3]

Notes / practical tips:

  • Some userscript managers (Tampermonkey historically exposed callbacks; newer APIs/other managers expose an async Promise-returning GM.download) explicitly document an onload callback and that the Promise resolves with the download details (and gives you an abort function). Use the onload handler if you need the raw event object synchronously. [1][2]
  • If you rely on event objects from the onload handler, copy needed fields inside the callback before returning/resolving — event objects/state may no longer be reliable after the async resolution. [4]

Sources:

  • Tampermonkey GM_download docs (callbacks + return details/abort; Promise behavior documented where supported). [1]
  • Violentmonkey/GM API notes showing GM.download is async (resolves). [2]
  • Example wrappers/gists that implement Promise by resolving onload and rejecting onerror. [3]
  • Discussion about event timing / copying event data in callbacks. [4]

References:
[1] Tampermonkey GM_download documentation.
[2] Violentmonkey Privileged APIs (GM.download async note).
[3] Example: GM_download wrapped as Promise (gist).
[4] Discussion about event timing when resolving promises (JS thread).


🏁 Script executed:

# Locate and examine the full fallback implementation context in pagemd.user.js
fd -t f "pagemd.user.js" | head -1 | xargs wc -l

Repository: cbkii/userscripts

Length of output: 80


🏁 Script executed:

# Read the pagemd.user.js file to understand the fallback context
sed -n '600,750p' pagemd.user.js | cat -n

Repository: cbkii/userscripts

Length of output: 5944


🏁 Script executed:

# Search for MOBILE_FALLBACK_DELAY_MS and fallback-related code
rg -n "MOBILE_FALLBACK_DELAY_MS|fallbackTriggered|triggerFallback" pagemd.user.js | head -30

Repository: cbkii/userscripts

Length of output: 616


🏁 Script executed:

# Find where fallbackTriggered is initialized and how triggerFallback works
rg -B5 -A10 "const triggerFallback|function triggerFallback" pagemd.user.js

Repository: cbkii/userscripts

Length of output: 435


🏁 Script executed:

# Find downloadViaAnchor and downloadViaDataUrl to understand fallback mechanisms
rg -B5 -A15 "const downloadViaAnchor|const downloadViaDataUrl" pagemd.user.js | head -80

Repository: cbkii/userscripts

Length of output: 1319


🏁 Script executed:

# Check what resource.markStale() and resource.cleanup() do
rg -B3 -A8 "resource\.markStale|resource\.cleanup|createDownloadResource" pagemd.user.js | head -100

Repository: cbkii/userscripts

Length of output: 2737


🏁 Script executed:

# Find MOBILE_FALLBACK_DELAY_MS definition and understand the timeout choice
rg -B5 -A5 "MOBILE_FALLBACK_DELAY_MS|DOWNLOAD_ANCHOR_DELAY_MS|BLOB_REVOKE_MS" pagemd.user.js

Repository: cbkii/userscripts

Length of output: 3376


Race condition can trigger duplicate downloads on slow-but-successful mobile downloads — the 1200ms timeout may fire before GM_download's onload callback, causing both the primary download and fallback anchor/data-URL download to execute.

The fallbackTriggered flag prevents duplicate cleanup operations but does not prevent the fallback download itself from initiating. If GM_download succeeds but the onload callback fires after 1.2 seconds on a slow mobile connection, the timeout will have already called triggerFallback(), which invokes fallback() and initiates the anchor/data-URL download. The subsequent onload callback will then return early, leaving two separate downloads in flight.

Consider implementing bounded retry logic with network-aware backoff (per the polling fallback pattern) or increasing the timeout threshold on mobile networks to allow more time for successful downloads to complete before falling back.

Manual test:
- On a mobile browser (Android/iOS), export Page → Markdown and confirm the download completes.
- On a mobile browser, export Page Info and confirm the .txt download completes.
- On a mobile browser, export a ChatGPT conversation and confirm the download completes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @chatgptmd.user.js:
- Around line 1479-1492: The download options object named detail in
chatgptmd.user.js sets saveAs: true which is inconsistent with pagemd.user.js
and pageinfoexport.user.js that use saveAs: false; change the saveAs property in
the detail object (the constant named detail where url, name, saveAs, onload,
onerror are defined) from true to false so all three scripts share the same
download behavior, and run a quick manual test of the download flow (including
fallbackTrigger and resource.cleanup) after the change.
🧹 Nitpick comments (3)
pagemd.user.js (1)

594-622: Consider extracting a reusable blobToDataUrl helper.

The inline Promise-based FileReader logic here duplicates similar patterns in pageinfoexport.user.js (line 926-931) and chatgptmd.user.js (line 1426-1437). While each script must be self-contained per coding guidelines, extracting this to a named helper function within this file would improve readability and maintainability.

♻️ Optional refactor to extract helper
+  const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
+    reader.onerror = () => reject(new Error('Failed to read blob as data URL'));
+    reader.readAsDataURL(blob);
+  });
+
   const downloadViaGM = async (resource, filename, fallback) => {
     // ...
     let downloadUrl = resource.getUrl();
     try {
       const blob = resource.getBlob();
       if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
-        const reader = new FileReader();
-        const dataUrlPromise = new Promise((resolve, reject) => {
-          reader.onload = () => resolve(reader.result);
-          reader.onerror = () => reject(new Error('Failed to read blob'));
-          reader.readAsDataURL(blob);
-        });
         try {
-          downloadUrl = await dataUrlPromise;
+          downloadUrl = await blobToDataUrl(blob);
         } catch (err) {
pageinfoexport.user.js (1)

990-1014: Use the existing blobToDataUrl helper here.

This inline FileReader/Promise logic duplicates the blobToDataUrl helper defined at lines 926-931. Consider using the helper for consistency and reduced duplication.

♻️ Proposed refactor to use existing helper
     // Mobile compatibility: prefer data URL for better download manager support
     // Blob URLs may fail silently on some Android browsers
     let downloadUrl = resource.getUrl();
     try {
       const blob = resource.getBlob();
       // Only convert to data URL if size is reasonable (< 2MB for mobile compatibility)
       if (blob.size < MAX_DATA_URL_FILE_SIZE_BYTES) {
-        const reader = new FileReader();
-        const dataUrlPromise = new Promise((resolve, reject) => {
-          reader.onload = () => resolve(reader.result);
-          reader.onerror = () => reject(new Error('Failed to read blob'));
-          reader.readAsDataURL(blob);
-        });
         try {
-          downloadUrl = await dataUrlPromise;
+          downloadUrl = await blobToDataUrl(blob);
         } catch (err) {
           log('warn', 'Failed to convert blob to data URL, falling back to blob URL', { error: err?.message || String(err) });
           // Fall back to blob URL if data URL conversion fails
           downloadUrl = resource.getUrl();
         }
       }
     } catch (err) {
chatgptmd.user.js (1)

73-79: Mobile constants are functionally consistent.

The MAX_DATA_URL_FILE_SIZE_BYTES is expressed as 2 * 1024 * 1024 here versus 2097152 in the other files. Both evaluate to the same value (2MB), but consider using a consistent expression across all files for easier maintenance.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a011a82 and 79da108.

📒 Files selected for processing (3)
  • chatgptmd.user.js
  • pageinfoexport.user.js
  • pagemd.user.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.user.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.user.js: Userscript metadata block must be positioned at the very beginning of the file with exact syntax: // ==UserScript== and // ==/UserScript== on their own lines, each metadata line starting with // (exactly one space after //)
Include required metadata keys: @name, @namespace (set to https://github.com/cbkii/userscripts), @version (datetime format YYYY.MM.DD.HHMM), @description, @author, @icon (base64 SVG data URI with #FF1493 hot pink stroke), @match or @include, and @run-at
Each userscript must include a base64-encoded SVG data URI icon in the @icon metadata field using a simple line-style icon with hot pink (#FF1493) stroke color, placed after @author and before @match
Use @match patterns with the narrowest practical scope (least privilege principle), preferring Chrome-style match patterns like *://example.com/* over broad patterns like *://*/*
Declare all GM APIs used in the @grant metadata field, including legacy (GM_getValue) or async versions (GM.getValue), and avoid implicit grants
For any cross-origin network requests using GM_xmlhttpRequest, declare required domains in @connect metadata entries; avoid @connect * unless truly necessary
Wrap script logic in a single top-level IIFE with 'use strict' directive and provide a clear main() entrypoint
Increment @version for every functional change using datetime format YYYY.MM.DD.HHMM (UTC), ensuring versions never decrease
Ensure DOM updates are idempotent (safe to run multiple times) by checking if initialization has already occurred, marking injected elements, and avoiding duplicate listeners
For any @match *://*/* or similarly broad patterns, implement dormant-by-default behavior: register menu commands and UI shell on init, but only run heavy work when Always Run is enabled or user explicitly triggers via UI
Use consistent logging with a single createLogger() helper per script, store logs under `GM_setValue('userscript.logs.<short...

Files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include clear manual test steps in PR/commit messages or `docs/<script>.md` documenting: happy path (expected behavior), negative path (missing elements, excluded pages), SPA navigation, and XBrowser compatibility checks
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Prioritize Android XBrowser compatibility above all other considerations; test on XBrowser and ensure scripts work with its built-in script manager
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use canonical CDN URLs for shared dependencies: jQuery from googleapis, jQuery UI from googleapis, Readability from jsDelivr, Turndown/GFM from unpkg (UMD builds, not CJS)
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : If hosting scripts with updates, set both `updateURL` (metadata URL) and `downloadURL` (full script URL) to raw GitHub URLs; increment `version` for any functional change

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Increment `version` for every functional change using datetime format `YYYY.MM.DD.HHMM` (UTC), ensuring versions never decrease

Applied to files:

  • pagemd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use a tiny compatibility wrapper (GMX pattern) to handle both GM4 async (`GM.getValue`) and legacy (`GM_getValue`) APIs, enabling cross-manager compatibility

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Prioritize Android XBrowser compatibility above all other considerations; test on XBrowser and ensure scripts work with its built-in script manager

Applied to files:

  • pagemd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Declare all GM APIs used in the `grant` metadata field, including legacy (`GM_getValue`) or async versions (`GM.getValue`), and avoid implicit grants

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Implement race condition mitigation when 12+ scripts run simultaneously: stagger initialization with `setTimeout(0)`, use short polling intervals with exponential backoff, batch MutationObserver callbacks, and test thoroughly with all scripts installed

Applied to files:

  • pagemd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include required metadata keys: `name`, `namespace` (set to `https://github.com/cbkii/userscripts`), `version` (datetime format `YYYY.MM.DD.HHMM`), `description`, `author`, `icon` (base64 SVG data URI with #FF1493 hot pink stroke), `match` or `include`, and `run-at`

Applied to files:

  • pagemd.user.js
  • pageinfoexport.user.js
  • chatgptmd.user.js
🧬 Code graph analysis (2)
pageinfoexport.user.js (2)
chatgptmd.user.js (5)
  • MOBILE_FALLBACK_DELAY_MS (76-76)
  • BLOB_STALE_MS (77-77)
  • BLOB_REVOKE_MS (78-78)
  • isMobileBrowser (73-74)
  • DOWNLOAD_ANCHOR_DELAY_MS (75-75)
pagemd.user.js (5)
  • MOBILE_FALLBACK_DELAY_MS (455-455)
  • BLOB_STALE_MS (456-456)
  • BLOB_REVOKE_MS (457-457)
  • isMobileBrowser (452-453)
  • DOWNLOAD_ANCHOR_DELAY_MS (454-454)
chatgptmd.user.js (2)
pageinfoexport.user.js (6)
  • isMobileBrowser (306-307)
  • DOWNLOAD_ANCHOR_DELAY_MS (302-302)
  • MOBILE_FALLBACK_DELAY_MS (303-303)
  • BLOB_STALE_MS (304-304)
  • MAX_DATA_URL_FILE_SIZE_BYTES (61-61)
  • blobToDataUrl (926-931)
pagemd.user.js (5)
  • isMobileBrowser (452-453)
  • DOWNLOAD_ANCHOR_DELAY_MS (454-454)
  • MOBILE_FALLBACK_DELAY_MS (455-455)
  • BLOB_STALE_MS (456-456)
  • MAX_DATA_URL_FILE_SIZE_BYTES (64-64)
🔇 Additional comments (14)
pagemd.user.js (4)

4-4: Version bump looks good.

The version format follows the required YYYY.MM.DD.HHMM datetime format and has been appropriately incremented for this functional change.


452-457: Mobile detection and fallback constants are well-aligned.

The broader mobile detection regex (/Android|iPhone|iPad|iPod|Mobile/i) and the MOBILE_FALLBACK_DELAY_MS constant are consistent with the other files in this PR. This appropriately extends XBrowser-specific handling to all mobile browsers.


624-688: Mobile fallback coordination logic looks sound.

The idempotent triggerFallback pattern with fallbackTriggered guard prevents double-fallback execution. The conditional promise handling (awaiting on desktop, timeout-based on mobile) correctly addresses the PR objective of avoiding silent hangs when GM_download callbacks are unreliable on mobile. Based on learnings, this polling fallback pattern aligns with XBrowser compatibility guidance.


641-654: Detail object correctly configured for mobile compatibility.

Setting saveAs: false and conditionally adding confirm: false on mobile helps avoid dialog prompts that may cause issues on mobile download managers. The onload and onerror callbacks are properly wired to the fallback coordination.

pageinfoexport.user.js (5)

6-6: Version bump consistent with other files.

Version 2026.01.10.0842 matches the other scripts in this PR.


303-308: Mobile detection constants consistent across the PR.

The MOBILE_FALLBACK_DELAY_MS and isMobileBrowser detection are identical to the other files, ensuring consistent behaviour across all three scripts.


926-931: Clean blobToDataUrl helper implementation.

This helper is well-structured with proper error handling. It's correctly extracted as a reusable function within this script.


968-988: Dual-promise coordination pattern for mobile fallback.

The pattern using both resolveLegacy and resolveMobileFallback promises allows the function to coordinate completion across different callback paths. The handleError function correctly signals both completion handlers.

One observation: resolveMobileFallback is set to null after being called, and subsequent checks rely on truthiness. This works correctly but could be slightly clearer with explicit null guards.


1054-1094: Mobile fallback timeout and promise coordination looks correct.

The timeout-based fallback (line 1055-1063) with subsequent promise handling correctly implements the mobile fallback pattern. The conditional awaiting (non-mobile awaits the promise, mobile relies on timeout) aligns with the PR objectives.

Minor note: clearFallbackTimer() at line 1094 is slightly redundant as the timer should already be cleared by earlier paths, but it's harmless defensive code.

chatgptmd.user.js (5)

4-4: Version bump consistent.

Version 2026.01.10.0842 matches the other scripts in this PR.


512-514: Export action correctly updated to use mobileDownload.

The download action now awaits mobileDownload, which handles both mobile and desktop download paths with appropriate fallbacks.


1426-1437: Robust blobToDataUrl helper with additional validation.

This implementation includes extra validation (typeof reader.result !== 'string' || !reader.result) before resolving, which is more defensive than the other files. This is good practice for handling edge cases.


1464-1518: Mobile fallback timer coordination follows established pattern.

The fallbackTimerId, fallbackTriggered, clearFallbackTimer, and triggerFallback pattern is consistent with the other files. The conditional promise awaiting correctly handles mobile vs desktop paths.


1522-1531: Error handling with user feedback is good.

The catch block properly marks the resource as stale, triggers the fallback, logs the error, and provides user feedback via alert(). This gives users actionable information when downloads fail.

Comment thread chatgptmd.user.js
Manual test:
- Unable to run in this environment (no mobile browser UI).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
chatgptmd.user.js (1)

1-22: Add missing metadata fields and declare all GM APIs used.

The metadata block is missing @license and @sandbox fields required by repository guidelines. Additionally, the script actively checks for and uses GM.download (async GM4 API, lines 70–71) but only declares @grant GM_download in the metadata; you must add @grant GM.download.

The script also references unsafeWindow (lines 101, 131) to access the shared UI helper across sandbox boundaries. If you declare @sandbox DOM, unsafeWindow will be inaccessible, creating a conflict. You must either:

  • Declare @sandbox JavaScript or @sandbox raw instead of @sandbox DOM, or
  • Remove the unsafeWindow fallback path for the UI helper
Proposed metadata additions
 // ==UserScript==
 // @name         ChatGPT Exporter for Android (md/txt/json)
 // @namespace    https://github.com/cbkii/userscripts
 // @version      2026.01.10.0850
 // @description  Export ChatGPT conversations to Markdown, JSON, or text with download, copy, and share actions. UI integrated with shared userscript panel.
 // @author       cbcoz
+// @license      MIT
 // @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkYxNDkzIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTIxIDE1djRhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJ2LTQiLz48cG9seWxpbmUgcG9pbnRzPSI3IDEwIDEyIDE1IDE3IDEwIi8+PGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjMiLz48L3N2Zz4=
 // @match        *://chat.openai.com/*
 // @match        *://chatgpt.com/*
 // @updateURL    https://raw.githubusercontent.com/cbkii/userscripts/main/chatgptmd.user.js
 // @downloadURL  https://raw.githubusercontent.com/cbkii/userscripts/main/chatgptmd.user.js
 // @homepageURL  https://github.com/cbkii/userscripts
 // @supportURL   https://github.com/cbkii/userscripts/issues
 // @run-at       document-idle
+// @sandbox      JavaScript
 // @noframes
 // @grant        GM_registerMenuCommand
 // @grant        GM_unregisterMenuCommand
 // @grant        GM_setClipboard
 // @grant        GM_getValue
 // @grant        GM_setValue
 // @grant        GM_download
+// @grant        GM.download
 // ==/UserScript==
🤖 Fix all issues with AI agents
In @chatgptmd.user.js:
- Around line 73-80: The size cap constant MAX_DATA_URL_FILE_SIZE_BYTES should
be used inclusively to avoid an off-by-one rejection at exactly 2 MiB; find the
data-URL/file-size check that currently uses a strict < comparison against
MAX_DATA_URL_FILE_SIZE_BYTES and change it to <= MAX_DATA_URL_FILE_SIZE_BYTES
(update any related conditional branches or early returns that depend on that
comparison).
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79da108 and 73ebe3a.

📒 Files selected for processing (1)
  • chatgptmd.user.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.user.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.user.js: Userscript metadata block must be positioned at the very beginning of the file with exact syntax: // ==UserScript== and // ==/UserScript== on their own lines, each metadata line starting with // (exactly one space after //)
Include required metadata keys: @name, @namespace (set to https://github.com/cbkii/userscripts), @version (datetime format YYYY.MM.DD.HHMM), @description, @author, @icon (base64 SVG data URI with #FF1493 hot pink stroke), @match or @include, and @run-at
Each userscript must include a base64-encoded SVG data URI icon in the @icon metadata field using a simple line-style icon with hot pink (#FF1493) stroke color, placed after @author and before @match
Use @match patterns with the narrowest practical scope (least privilege principle), preferring Chrome-style match patterns like *://example.com/* over broad patterns like *://*/*
Declare all GM APIs used in the @grant metadata field, including legacy (GM_getValue) or async versions (GM.getValue), and avoid implicit grants
For any cross-origin network requests using GM_xmlhttpRequest, declare required domains in @connect metadata entries; avoid @connect * unless truly necessary
Wrap script logic in a single top-level IIFE with 'use strict' directive and provide a clear main() entrypoint
Increment @version for every functional change using datetime format YYYY.MM.DD.HHMM (UTC), ensuring versions never decrease
Ensure DOM updates are idempotent (safe to run multiple times) by checking if initialization has already occurred, marking injected elements, and avoiding duplicate listeners
For any @match *://*/* or similarly broad patterns, implement dormant-by-default behavior: register menu commands and UI shell on init, but only run heavy work when Always Run is enabled or user explicitly triggers via UI
Use consistent logging with a single createLogger() helper per script, store logs under `GM_setValue('userscript.logs.<short...

Files:

  • chatgptmd.user.js
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Consult and apply the ANDROID-XBROWSER-TESTING.md and ANDROID-XBROWSER-FIXES-TECHNICAL.md documents when designing or fixing scripts for XBrowser, especially for polling fallbacks, download functionality, and load order independence
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Prioritize Android XBrowser compatibility above all other considerations; test on XBrowser and ensure scripts work with its built-in script manager
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use canonical CDN URLs for shared dependencies: jQuery from googleapis, jQuery UI from googleapis, Readability from jsDelivr, Turndown/GFM from unpkg (UMD builds, not CJS)
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : If hosting scripts with updates, set both `updateURL` (metadata URL) and `downloadURL` (full script URL) to raw GitHub URLs; increment `version` for any functional change

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Ensure compatibility with XBrowser (Android) by: sticking to documented Tampermonkey APIs, avoiding obscure/experimental features, keeping syntax ES2018-compatible, and using simple manual install paths

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Use the polling fallback pattern for XBrowser compatibility: when APIs fail (e.g., `GM_download`, `fetch`), fall back to polling-based alternatives with bounded retry counts and clear backoff strategies

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use recommended metadata fields: `license`, `noframes`, `homepageURL` (set to `https://github.com/cbkii/userscripts`), `supportURL` (set to `https://github.com/cbkii/userscripts/issues`), `updateURL`, and `downloadURL` (both pointing to raw GitHub URLs)

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Declare all GM APIs used in the `grant` metadata field, including legacy (`GM_getValue`) or async versions (`GM.getValue`), and avoid implicit grants

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use English-only comments and user-facing strings throughout the script

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include clear manual test steps in PR/commit messages or `docs/<script>.md` documenting: happy path (expected behavior), negative path (missing elements, excluded pages), SPA navigation, and XBrowser compatibility checks

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Include required metadata keys: `name`, `namespace` (set to `https://github.com/cbkii/userscripts`), `version` (datetime format `YYYY.MM.DD.HHMM`), `description`, `author`, `icon` (base64 SVG data URI with #FF1493 hot pink stroke), `match` or `include`, and `run-at`

Applied to files:

  • chatgptmd.user.js
📚 Learning: 2026-01-05T10:24:18.701Z
Learnt from: CR
Repo: cbkii/userscripts PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T10:24:18.701Z
Learning: Applies to **/*.user.js : Use a tiny compatibility wrapper (GMX pattern) to handle both GM4 async (`GM.getValue`) and legacy (`GM_getValue`) APIs, enabling cross-manager compatibility

Applied to files:

  • chatgptmd.user.js
🧬 Code graph analysis (1)
chatgptmd.user.js (1)
pagemd.user.js (6)
  • isMobileBrowser (452-453)
  • DOWNLOAD_ANCHOR_DELAY_MS (454-454)
  • MOBILE_FALLBACK_DELAY_MS (455-455)
  • BLOB_STALE_MS (456-456)
  • BLOB_REVOKE_MS (457-457)
  • MAX_DATA_URL_FILE_SIZE_BYTES (64-64)
🔇 Additional comments (2)
chatgptmd.user.js (2)

513-514: Awaiting mobileDownload(...) is a good control-flow clean-up.

It makes the download action sequencing more predictable (especially now that mobile paths may do async blob→data URL work).


1426-1437: blobToDataUrl helper is fine; keep it ES2018-safe for XBrowser.

This uses FileReader, which is broadly compatible; just ensure you don’t rely on newer syntax/features around it on the mobile target browsers. Based on learnings, prioritise XBrowser compatibility.

Comment thread chatgptmd.user.js
Comment thread chatgptmd.user.js
Manual test:
- Unable to run in this environment (no mobile browser UI).
@cbkii cbkii merged commit 6299926 into main Jan 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant