The project follows Cosmic Gemini’s Central → Province → Product separation. background/central.js routes events, never parses documents. Standing Province owns automatic opening and per-tab native-reader exemptions. Operations Province owns extension administration and serialized preference writes. Customs Province owns the privileged AI explanation product as well as the document workspace boundary. The reader/renderer/OCR workspace is the Customs boundary: it handles imported bytes but cannot invoke sibling products. The host exposes a small explicit command bridge, not arbitrary RPC. The three province names describe ownership, not a reason to add empty lifecycle machinery.
Preferences are normalized against a whitelist in core/settings.js. Unknown/deprecated fields are ignored, never migrated. Defaults are automatic appearance, 100% initial zoom, 4× sampling, sharpening off and on-request English/Simplified Chinese OCR. Interface languages are en-US/zh-CN. Recognition languages additionally include Traditional Chinese. Open readers snapshot rendering quality and OCR language/detail settings; subsequent opens/reloads use changed settings. Appearance controls change the current reader only. All toolbar modes use one sun/moon button for the saved default or its opposite, with a stable switch-light/dark tooltip. Auto follows the browser in either state; changing toolbar layout leaves the override unchanged. Toolbar visibility changes synchronize to open readers without reparsing their PDFs; rendering quality and OCR language/detail preferences remain snapshots. Interface language, document-date format, Find shortcut preference and OCR button behavior update immediately without reparsing or restarting recognition.
Chrome 128+ response-header declarativeNetRequest rules redirect top-level GET PDF responses to an extension page. A higher-priority allow rule preserves attachment downloads. HTML named .pdf does not match unless its MIME type is PDF; generic binary data requires a PDF suffix. POST bodies, embedded documents and site blob URLs are not intercepted. File URL rules require Chrome’s explicit file-access permission.
A DNR regex capture cannot URL-encode its substitution. The final ?source= value is therefore parsed as the complete raw suffix, preserving all original query parameters and fragments. Only HTTP/HTTPS/file schemes without credentials are accepted. The privileged host is top-level only, fetches the requested PDF with session credentials, enforces time/byte budgets and validates PDF magic. Fetching again can fail for one-use/authenticated endpoints; no attempt is made to replay POST requests or read browser credentials.
Native-reader requests must come from frame 0 of this extension’s reader tab. Central derives the URL from the sender, installs a high-priority exact-URL tab-scoped session allow rule, then navigates. Exemptions survive PDF reloads, not tab departure/closure or browser restart. Other tabs remain unaffected. Very long unsupported regex URLs fail with guidance rather than creating a redirect loop. Local file-picker documents use a blob URL opened in a separate Chrome-reader tab while the original owner remains open.
PDF URLs remain in the reader address for reload, not in extension storage. Native exemption metadata lives in storage.session only. Local picked bytes are not persisted. Download always returns the original PDF, never the displayed dark/rotated/OCR representation.
The homepage Settings link navigates in the same tab and adds a history entry. Reader-toolbar Settings opens in the same window immediately after the source PDF tab, preserving local file-picker documents. The background derives the source tab from the extension message sender; the sandbox cannot supply an arbitrary tab target. The PDF viewport contains vertical overscroll but leaves horizontal browser navigation enabled; Alt-key history shortcuts are not consumed. History restoration recreates a disposed renderer from in-memory bytes or its validated source URL.
Settings is a single-tab surface per normal/incognito context. Each trusted top-level Settings document registers its document-start time (or BFCache return time). A small session registry of live document IDs/times survives background restarts; it stores no URLs/content and is pruned on registration. Serialized cleanup uses runtime.getContexts() to inspect only extension-owned documents, chooses the newest registered document regardless of load order and rechecks document identities before closing older tabs. Unregistered loading documents await their own registration so a slow newer page is not closed accidentally. No polling or new permissions are needed. Homepage-to-Settings and Settings-to-homepage navigation both remain same-tab with browser history; a PDF reader still opens its replacement Settings tab immediately on its right.
Opening a PDF prepares the sandbox toolbar independently of document transfer. The shell is revealed as soon as localization, appearance and toolbar preferences are applied. Loading animates the toolbar’s bottom divider without adding a status row or changing content geometry. Document-dependent controls stay disabled until usable; theme, Settings and remote-source native fallback work during transfer. A single bounded document message follows shell initialization over its private port. The bundled parser worker starts concurrently with download; optional thumbnails wait for the first page’s render. Downloads still complete and pass size/magic validation before parsing. The validated source URL enters the sandbox only as inert Document properties text, with no website network capability. No renderer or OCR engine starts on the empty homepage. Parsed background documents wait for visibility without a false first-paint timeout. Failures dispose the shell/worker and expose normal recovery actions.
The manifest declares only storage and declarativeNetRequestWithHostAccess. Storage holds preferences and bounded session metadata. Request rules require the corresponding declared host access; the extension has no additional host-independent blocking/allowing permission. HTTP/HTTPS host access is required to automatically recognize top-level PDF responses on arbitrary websites and retrieve their original bytes. file:///* supports local URL takeover only when Chrome’s existing file-access switch allows it; choosing or dropping a file works without that switch. There are no optional permissions or runtime permission requests.
Saving uses a temporary download anchor owned by the trusted reader host, pointing only to its existing original PDF Blob with a sanitized filename. It neither refetches the source nor reads/manages download history. Chrome’s download preferences decide whether to ask for a destination; the extension does not force a Save As dialog. No downloads, tabs, webRequest, scripting, management or clipboard extension permission is requested. The sandbox receives no download URL or extra capability. Permission additions are rejected by the package checker until this boundary is explicitly reviewed.
The extension-origin host is outside normal website content-script matching. This is how it prevents Dark Reader and similar webpage transformations from double-applying; it never disables or changes other extensions. A darkreader-lock meta tag is defense in depth, not the security boundary.
An opaque manifest-sandbox iframe receives bytes once through a transferred MessagePort. It has no extension APIs or preferences storage. Its only knowledge of the validated document source URL is an inert text label for Document properties; a locally selected file has no source URL. Its CSP permits only bundled/blob resources, no external fetch, frames, form submission, objects or evaluated JavaScript. PDF.js scripting/XFA/form/editor/attachment layers are not enabled. Text and a bounded set of passive safe links are separate from drawing. External Links Capture validates navigation URLs, including application protocols; remote document resources remain blocked. PDF copy/print restrictions are respected by the reader and OCR. No claim of absolute immunity to parser vulnerabilities is made.
The privileged bridge recognizes only readiness, fixed toolbar actions and theme/fullscreen. OCR copying uses browser selection, with no privileged copy command and no clipboard-write permission. Explicit AI explanations separately pass only bounded selected text and optional nearby context through the private host bridge. It never accepts arbitrary URLs, file paths, API names or HTML from the sandbox. Rendering workers use packaged code, not file-supplied scripts. Dependency bytes and licenses are checked against integrity manifests.
PDF input: 64 MiB, 60-second fetch deadline, 30-second parse deadline outside password entry, at most 10,000 pages. Byte streaming is stopped on overflow. Images are capped by PDF.js. The original Blob remains only for native opening/download while the reader is open.
Sampling choices 1×–6× change backing density, not CSS zoom. Base canvases stay within min(4 Mi pixels, sampling² Mi pixels); visible detail canvases within min(36 Mi pixels, sampling² Mi pixels), maximum dimension 8,192. The inherited reproducible PDF.js patch uses exact backing/display ratios and bounded near-viewport detail. Zoom moves in ten percentage points, wheel changes are coalesced and rasterization delayed briefly; no reparse on zoom. Custom zoom labels update immediately, but their option is located between neighboring presets only on pointer/keyboard menu opening (once per changed value). Hidden tabs do not schedule normal rendering. Thumbnails are lazy, at most 24 small canvases; print is bounded to 50 pages and 64 Mi pixels per job. Default sharpening is off. Theme changes filter displayed canvases only and leave source bytes unchanged. Offscreen detail, workers, observers, blobs and print surfaces have explicit cleanup.
The Preserve dark-paper colors row is hidden only when the saved Appearance is explicitly Light; Auto keeps it available regardless of the current browser theme. This changes only Settings visibility, never the stored preference or an open reader’s appearance override.
The default-on preserveDarkPaper switch controls a deliberately narrow black/gray-paper exception, not a semantic photo/text classifier. A 32×32 whole-page preflight rejects white and light-gray paper; only rare dark-edged candidates receive 128×128 and 257×257 whole-page confirmation. The pixel-only route requires at least 97% neutral background pixels within six luminance levels of the dark perimeter shade (below midgray), low background variance, strict per-tile limits, an entirely dark outer perimeter and at least 99.5% matching background pixels in each edge band.
Dense layouts that fail the pixel-only route can use additional evidence from the pinned PDF.js display cache. The adapter reads at most the first 48 already-rendered operators, with a maximum save depth of eight. It accepts only a neutral dark fill as the first painted object covering the whole paper, checking the actual cached path and preceding clips on a 32×32 scratch surface rather than trusting its bounding box. Missing/ambiguous cache state, earlier white paint, unsupported transforms/graphics state and non-covering paths fail closed. It never calls getOperatorList(), starts a second operator stream or retains the cache. Dependency upgrades must retest this private cache adapter.
With that evidence, both whole-page samples still require an uninterrupted matching dark perimeter, at least 35% matching neutral paper, low background variance and a low page mean. A per-tile white limit and bounded one-row solid-white-block scan veto light content panels even across tile boundaries; text and colored callouts near the paper edge are allowed. A white-paper margin around a dark photograph or star field always vetoes preservation. Ambiguity, textured/colored paper beds, insufficient raster size and read/render errors retain normal inversion. Only paper shades that would become brighter at the frozen dark strength are exempt; lighter gray paper keeps normal processing. Preserving source colors avoids contrast loss from extra dimming. This favors missed exceptions over falsely exposing white paper; neither finite visual samples nor paint structure can prove document semantics.
The detector reuses a completed full base canvas, never a cropped detail/thumbnail, OCR or an extra PDF render. Disabling the switch synchronizes to open readers through the private host port, removes the display exception immediately and stops analysis; enabling reuses cached verdicts without reparsing. Light mode does no image analysis; entering dark mode checks only finished cached page views. Each page is decided at most once per reader, including failures; zoom/rotation and matching thumbnails reuse the result. The cache is one byte per page (at most 10,000 bytes), with a single reusable scratch canvas no larger than 257×257, zeroed after each decision. Unknown dark canvases remain concealed until their completed render can be checked before paint; known pages retain progressive rendering. No whole-document scan, timer or persistent content cache is introduced. Paper boundaries, text selection, OCR, printing and downloaded bytes are unchanged.
Document properties are imported and read only on toolbar request, with one cached metadata read per open reader. Values are bounded plain text; the original file URL appears directly below File name when available, never as an active link. No metadata HTML, URL or script executes. File size uses received bytes and decimal units. Page dimensions include the original PDF page rotation and UserUnit, not the temporary viewing rotation. Date formatting preserves each source wall time and explicit UTC offset (without pretending a missing offset is known). Whole-hour offsets use compact notation such as UTC+8; nonzero minutes remain, such as UTC+5:30. Seconds are shown only when present in the PDF date, including zero seconds. Locale-based defaults retain the English medium date/short time convention, with spaced Chinese year/month/day; numeric date-order formats are selectable. Metadata is never reread for language/format changes. The modal keeps its established dimensions and closes on an outside press/click, not on a drag that starts inside it.
The useChromeFind switch defaults on, preserving native Chrome keyboard behavior: ⌘F/Ctrl+F are neither cancelled nor simulated. The toolbar/More actions Find button always opens PDF.js whole-document search, regardless of this setting. Turning the switch off also routes the shortcuts to built-in search. The preference updates live without dismissing an already-open reader search. Search matches use orange overlays, with a stronger current match in both themes; forced-colors palettes remain native. No shortcut banner or simulated browser command is used. Chrome only searches text currently present in the DOM (including completed OCR overlays); the extension does not force all pages into memory to expand its reach.
Settings and the reader share allowlisted language checkboxes. At least one language remains selected; any combination of English, Simplified Chinese and Traditional Chinese is supported. English and Simplified Chinese are selected by default. Only selected models are initialized. Defaults apply to newly opened readers; per-document choices do not rewrite the saved defaults.
Tesseract runs after a user recognition action, or on the stable current page when AI explanations are enabled/usable and the successfully rendered native text layer is empty. The latter is coordinated by automatic-ocr.js: rendering/page/readiness/visibility events feed one 500ms debounce; it never scans the whole document, polls, or interrupts manual work. Empty/error/cancel outcomes are not automatically retried; explicit recognition remains the retry path. Successful results are reused and may be rebuilt only after normal memory-budget eviction when the page is revisited. Explicit Clear does not immediately regenerate cleared results. AI switch-off cancels an unfinished automatic job, not a manual job; a manual action can take ownership of an active automatic job. Hidden pages do not start another job. The toolbar defaults to recognizing the current page; next5/next10 mean current plus exactly five/ten following pages, clipped at EOF. A 550ms hold opens the side panel without a duplicate click, including when the button is in More actions. The alternative panel behavior opens it on a normal click. Closing the panel never cancels an active run; both panel and compact feedback offer explicit cancellation. Each run handles a selected range of at most 20 pages sequentially using one LSTM worker, terminated after completion, error, cancellation or reader closure. Worker ownership starts before model loading, so Cancel is effective during initialization too. Each stage/page has a 120-second deadline. English, Simplified Chinese and Traditional Chinese models and both SIMD/non-SIMD LSTM cores are local; no cache/CDN/telemetry is used.
OCR rasterization uses the original light PDF page, independent of display sampling, with a 4 Mi pixel/4,096-axis limit. Temporary canvases are zeroed after encoding. Lightweight results are bounded to 20 pages, 50,000 word rectangles and 500,000 text characters. Word rectangles are validated and stored in PDF coordinates, enabling selectable overlays to follow zoom and rotation without rerunning recognition. Each completed page is overlaid immediately; no separate transcript is kept or shown. Line boundaries and English word spacing are preserved without inserting spaces between recognized Han glyphs. A user recognition request gives the new OCR text priority for the entire recognized page, without a text-quality heuristic or separate preference. A nonempty overlay is published atomically and removes the native text layer from rendered layout with display: none, excluding it and its descendants from Chrome Find and selection even if PDF.js later recreates the layer. Empty or whitespace-only results do not suppress native text. Clearing results, cache eviction and disposal restore the original layer; other pages remain unchanged. Cancellation or failure before publication preserves the existing layer. The original PDF is not modified; OCR does not export a searchable PDF. The engine’s worker protocol is pinned and tested; upgrades must retest initialization, recognition and cancellation.
English/Simplified Chinese README and changelog have equivalent structure. README introduces capabilities, not a catalog of implementation rules. Technical details live here; repeatable validation lives in QA.md. sidenote/ is ignored local continuity, not release history or distributable content. System .DS_Store/AppleDouble files are ignored by Git and validation. Dependencies are vendored, pinned, licensed and reproducible; no runtime remote code. No release archive is created unless requested. A new remote must be supplied explicitly; this repository must never push into Cosmic Gemini’s remote by inference.
Use the Cosmic Gemini family palette, flat outlined icons, compact identity header and rounded cards. Settings labels/controls and OCR choices are 15px; supporting text is at least 14px, except the opening-screen copyright at 13px. Preserve contrast in both themes and reflow at narrow widths rather than shrinking type. The editable brand is extension/icons/icon.svg; regenerate manifest PNGs with scripts/render-icons.mjs using PDF_PLAYWRIGHT/PDF_CHROME. Generation also fingerprints all first-party HTML favicon/brand URLs from the SVG content and records source/raster hashes in extension/icons/integrity.json. This changes the favicon cache key only when the artwork changes, without runtime work. scripts/check.mjs rejects stale source/raster hashes, incorrect PNG dimensions, missing favicons and old HTML icon references. Settings displays the version from chrome.runtime.getManifest(), never a hardcoded release number.
Two independent toolbar-content checkboxes allow both items, branding only, filename only, or neither. Filename defaults to visible and branding to hidden; existing explicit choices remain unchanged. Selecting both retains the two-row layout. Hiding either item uses a single row when space permits. The appearance button always precedes Print. Pages stays at the leading edge unless explicitly moved into the menu: on the second row when both identity items are shown, otherwise in the first-row leading cluster. Branding-only or filename-only content is left-aligned after Pages with a 12px gap, before the centered tools. Filename-only mode is left aligned, bounded to 320px or available space, and ellipsized with the full name in its tooltip. Narrow windows reflow controls without reducing text or button spacing. Current and total page counts use identical centered widths based on the total page count; the slash is a separate separator.
Toolbar action visibility is an allowlisted map of explicit overrides. Find, Previous/next page, Fit width/page, Print, Document properties and Use Chrome reader default to More actions in all four identity modes; explicit choices override these defaults independently of layout. The native-reader menu label is shortened to Chrome reader. The filename is also a keyboard-accessible properties button: disabled until parsing completes, it shares the same lazy dialog and cached metadata as the properties action. The dialog is capped at 560px in width and height within the viewport, with unchanged 15px body text. It autofocuses its heading and resets scroll offsets only on opening, never shifting an already-open dialog during locale updates. No hidden actions means the Settings button navigates directly; otherwise it opens an on-demand icon-labeled menu plus Settings. Menu commands delegate to the original controls and their disabled state; no new privileged bridge commands are introduced. A hidden fit selector is replaced with a 66px percentage input accepting optional %, clamped to the existing 25–500% range. Open menus support keyboard navigation, Escape, outside dismissal and a second trigger click. Trigger pointerdown records whether the auto popover was open, preventing light-dismiss-before-click from reopening it; keyboard clicks use the current state. Their intrinsic width follows the longest current localized caption, bounded by available viewport space; no continuous measurement is needed. Settings labels mark factory-default hidden actions, independently of current overrides.
Compact wrapping uses the intrinsic widths of visible toolbar groups and side actions, not a fixed viewport breakpoint. Side tracks balance the reading controls where possible, while filename-only mode reserves a 96px minimum before ellipsis. A toolbar-only ResizeObserver batches geometry reads into animation frames and disconnects on disposal; it never scans document pages or runs on PDF scrolling. Settings changes measure once before presentation, and later resizes, font geometry and page-count widths trigger remeasurement. The full filename-and-branding layout keeps its two rows, with an extra narrow-window row only when necessary to avoid overlap.
Visible-page rendering progress reuses the rendering queue’s existing visible-page set. A single cancellable 200 ms delay drives the existing toolbar-divider line; cached/fast pages and background prefetch cause no indicator. Page-interior PDF.js loading pseudo-elements are suppressed without changing rendering priority, allocations or document layout. Visibility/disposal clears pending feedback.
When document links are enabled, an explicit annotation or outline link click lazily loads a compact capture dialog inside the existing PDF sandbox. It reuses the Mailto Capture family parsers/presentation for email/telephone/SMS, with Open/Copy for ordinary web addresses and Open application/Copy for other validated external protocols. Internal PDF destinations remain internal. The separate default-on captureLinks preference updates open readers immediately; disabling it dismisses an open web prompt and restores direct safe web-link opening, without weakening the document-links gate. Mailto/tel/sms always use a copy-only prompt regardless of captureLinks, never expose an external href or launch an application. No telephone-location dataset, page observer or persistent link state is added. Web annotation anchors carry no external navigation target while capture is on; protocol annotations never carry one. Until a permitted web or application Open choice, middle/modifier clicks also enter capture. URLs are capped at 8 KiB and validated as external navigation targets, rejecting credentials, control/NUL values, script/data/blob/local-file targets and internal/privileged browser schemes. Decoded fields are plain text only. Application protocols always require the confirmation prompt, even when direct web opening is selected. Opening uses a trusted second click with noopener/noreferrer; copying remains inside the sandbox, with no new host command, extension permission or source network request. Closing or disposing removes the transient content. Interface language and reader theme apply to the prompt.
The opaque reader uses worker.js to adapt the pinned, self-contained PDF.js worker module into a classic blob worker: remove its checked export footer and resolve import.meta.url to the fixed packaged asset. This avoids Chrome’s opaque-origin module-worker redirect failure. The adapter waits for the native ready message, passes its port to PDFWorker, and explicitly owns termination/blob disposal on failure, abort and normal teardown; it never falls back to a main-thread fake worker. Startup has a bounded deadline. Dependency upgrades must revalidate the adapter and real-worker browser assertion.
The fixed reader iframe delegates only the clipboard-write Permissions Policy feature (opaque origin requires *); clipboard read remains disabled and no clipboard permission is added to the extension manifest. PDF.js whole-document Copy and link-copy fallbacks stay local, without sending document text through the privileged bridge. Dialog opening temporarily makes the dialog inert during native initial-focus selection, restores it synchronously and explicitly focuses the heading/input/button without scrolling. Native modal isolation, Escape and return focus remain intact; no cross-origin autofocus attributes are used.
Both pinned Tesseract core glue files decode embedded base64 WASM using their existing local byte decoder before any fetch/XHR branch. The guarded vendor-ocr.py patch is documented and integrity-pinned; model/WASM bytes, error reporting and local-only CSP are unchanged. Recognition diagnostics such as detected diacritics are informational, not failed OCR.
Settings puts the product name above the localized Settings subtitle. More actions choices reuse reader glyphs as decorative icons, with default-hidden annotations in the standard muted color and unchanged accessible checkbox labels.
The internal capability is named External Links Capture. Its pure protocol parser, target policy and sandbox-local modal live under shared/external-links-capture/, shared in implementation with Cosmic Gemini. Mailto Capture uses the same classic-compatible parser without importing the viewer UI or changing its website settings. Application Open is a browser handoff, subject to installed handlers and browser/OS permissions; it is never automatic. No PDF JavaScript/Launch action evaluator or arbitrary-URL privileged bridge is added.
Recognized words retain engine block/paragraph/line reading order, with left-to-right ordering inside each horizontal line. Each cached word carries its row identity and PDF-space corners. The selectable overlay uses inline word slots in each row within one shared rotated coordinate system, shared vertical bounds, and explicit line breaks, instead of unrelated absolute word boxes. Slots cover interword gaps; transparent glyphs retain individual horizontal scaling. Zoom and rotation rebuild only the bounded overlay from cached coordinates, never OCR. Repeated page-render events with the same result and viewport geometry preserve the existing DOM and active selection. No pointer-move selection handlers, content scans, or new settings are added.
Settings writes are serialized and applied to a fresh normalized base. Pending edits remain visible across locale-driven DOM rebuilds and incoming storage events. A delayed save callback must not republish a stale snapshot; post-write readback and an observation revision protect the latest received state. Failed writes remove only their own pending edit and do not poison later writes. Ordinary updates preserve control focus. Only automatic-opening changes reconcile takeover rules; cosmetic settings do not rewrite network rules.
Native-reader handover validates the sender’s current document ID before installing its tab/source exception and again before navigation. Cleanup ownership is persisted first. Installation/navigation failures roll back previous rules and metadata; failed rollback retains ownership for cleanup. Closing a tab also removes a deterministic orphan exception if its metadata is absent.
Reopened native dialogs reset their return value so Escape cannot repeat a previous Print/Open action. Modal keyboard events cannot navigate the underlying PDF. Page-number inputs accept whole page numbers and preserve the current page on invalid input.
core/ai.js holds public provider configuration, limits and prompt construction. Customs Province routes AI work to background/products/ai-explanation.js; Central only authenticates/routes fixed commands and notifies preference changes. Provider calls live in ai-client.js, while ai-vault.js owns credentials. The trusted reader host bridges normalized requests through a dedicated runtime Port; the opaque renderer receives public status and answer text, never a credential or arbitrary network capability. AI is off by default; Gemini is the initial provider, explanation language follows the interface, and nearby context defaults on. Explicitly saved context preferences are preserved; no migration overrides them. The neutral Satellites-style sparkle switch sits immediately left of appearance. Its slash/dim state signals Off. AI preferences synchronize live without restarting PDF parsing or OCR.
Provider/model IDs are fixed: gemini-3.5-flash-lite, gpt-5.6-luna and claude-haiku-5-5. Requests go only to their declared HTTPS API endpoints. Keys travel in API headers, never URLs, settings snapshots, logs, model prompts or PDF sandboxes. Fetch omits cookies/referrers, disables redirects/caching and never retries or falls back to another paid provider automatically. The connection-test button deliberately sends one manual, non-streaming JSON generation request with a 32-token output budget to the same fixed model. Gemini uses :generateContent; OpenAI/Claude set stream:false. It includes no document text, costs follow the API’s rules, and saving a key makes no provider request. A successful returned text is required; the test does not depend on SSE completion or an exact literal OK. No account sign-in, telemetry or extension server is involved. Provider API quotas, fees and data policies apply, including Gemini free-tier distinctions.
A nonextractable AES-256-GCM CryptoKey lives in extension-owned IndexedDB, together with versioned encrypted provider records. Every save uses a fresh random 96-bit IV; authenticated additional data binds the ciphertext to its provider. Vault operations are serialized; writes acknowledge transaction completion. Only the trusted top-level Settings page may save/test/delete credentials. Readers see boolean availability only. Settings lists saved keys by service without decrypting, displaying or refilling their values. A separate Add/Replace editor holds only a transient password draft; cancellation, method changes and page departure clear it. Each row has Test/Stop, Replace and confirmed Delete; its test result is local to that Settings instance and is not proof of future account/model access. The selected answer model is independent of the key editor’s service. There is no bulk-delete UI. Deleting the final provider key clears the encryption root as well; the existing internal clear-all command still atomically clears the store and cancels active work. Replacing/deleting a key cancels active work for that service. Future explicit saves generate a new root. Reset settings resets preferences but does not delete credentials. Encryption is local at rest, not an OS keychain, hardware-backed vault or a defense against compromised privileged extension code or a compromised profile. Built-in API explanations are unavailable in private browsing; neither private readers nor private Settings reuse or persist API credentials. Website mode may use the private window’s own website login without accessing API credentials.
Selected native/OCR text must originate in visible PDF text layers, respect PDF copy permissions and stay within 8,000 characters. Selection uses the browser’s visible-text representation rather than Range.toString, which can include suppressed native layers between OCR pages. Context captures at most 1,500 characters on each side of the selected range from its active layers and is sent only while Include nearby paragraphs is enabled; OCR rows retain line boundaries and never mix in native text from the same page. Other unrecognized pages retain their original text. The background also strips context when its trusted preference is off. No automatic full-document extraction, upload, filename/source-URL field or whole-document OCR/page scan is added. Empty current pages use the bounded local automatic OCR described above. API conversation state is scoped to one passage in one reader and discarded at disposal. Follow-ups retain at most ten alternating prior messages and 16,000 characters; questions cap at 1,500 characters.
selection.js clears only PDF reading ranges on primary clicks, window focus loss (including still-visible split panes) and tab hiding. Explain with AI preserves the current selection until it captures a private text/context snapshot; focus loss after handoff clears highlighting without losing the question or cancelling work. Right-click Copy and Shift-click extension remain native. AI-reply and form-field selections are untouched. Host blur forwards only a clear-selection signal over the private channel, never text. No tab monitoring, polling or extra permission is introduced, and listeners retire with the reader lifetime.
The shared explanation goal is understanding selected text, not summarizing a PDF passage. A word/name/abbreviation, longer selection, code or equation may call for different depth and structure; the model can use relevant background, examples, analogies and connections without a fixed answer sequence. Missing context does not require refusing a standalone term: the prompt requests a useful explanation and an explicit interpretation or alternatives where ambiguity matters. It does not name the file format or imply access to the full document. Supplied content stays distinct from background knowledge; unsupported author intent, fabricated citations and silently repaired OCR are discouraged. API replies use plain text because the reader does not render Markdown; website replies may use the website’s formatting. These choices follow the task/context separation described in OpenAI’s prompt guide, ChatGPT’s prompting guide and Claude’s prompting guidance. Construction tests do not establish model answer quality.
Prompt-injection boundaries: provider system instructions are separate API fields/roles, while the first user message naturally asks “Please help me understand the text I selected…” and supplies the reading material. Selected text (selected_text) and optional context are JSON-quoted document_data, explicitly labeled untrusted even if they contain forged roles, closing tags or instructions. They cannot create provider system/developer/tool messages. The model is instructed to explain such directives as content and never act on them. This reduces instruction confusion; it is not a guarantee that a probabilistic model always follows instructions. The enforceable boundary is no model tools, credential access, remote document fetching, command execution or automatic navigation. All model output uses textContent, never HTML/Markdown execution, URL actions or privileged commands.
SSE decoding preserves chunked UTF-8/CRLF and accepts only text deltas, skipping reasoning. Output caps at 16,000 characters, events at 64,000 characters and the response stream at 512,000 bytes. Errors become fixed localized codes, never raw provider bodies. Bounded error JSON (16 KiB) is examined locally for structured status/type/code/ErrorInfo reasons: Gemini 400 API_KEY_INVALID is authentication, FAILED_PRECONDITION is project/account setup, SERVICE_DISABLED is an API setup issue, 403 is access restrictions, 404 is model availability, 429 is quota, 400/422 is request configuration and 5xx is a service failure. Fetch rejection is network and malformed/truncated responses are incomplete, not claims that an otherwise valid key is wrong. Connection JSON is bounded to 64 KiB. Unknown/raw messages, request IDs, credentials and arbitrary metadata are never sent to UI/logs. Explanation jobs have a 25-second first-text/idle budget and 90-second overall deadline; connection tests have 25 seconds and report deadline expiry as timeout, not user cancellation. Only one test per Settings tab may remain active; Stop is authenticated/scoped to that tab and provider. A heartbeat exists only during active user work. Stop, switch-off, key deletion, replacement selection, tab navigation and renderer disposal abort work; obsolete IDs/ports cannot repaint a newer explanation. Visible work shares the toolbar-divider progress line. There is no background polling or persistent text history.
The error categories follow Gemini troubleshooting, OpenAI API errors and Claude API errors. Gemini keys can be subject to project/service and application restrictions, as described in Google’s key guide. An invalid dummy-key endpoint check proves reachability and error mapping only; it does not verify any user’s valid key, project billing or model access. Model selection is fixed; requests never fall back to a different model automatically.
Gemini uses the stable gemini-3.5-flash-lite model for explanations and connection tests. GenerateContent requests set thinkingConfig.thinkingLevel to minimal, not the older Gemini 2.5 thinkingBudget; minimal reasoning does not guarantee thinking is disabled. See Google’s GenerateContent thinking guide. The provider storage identifier remains gemini, preserving saved encrypted keys.
Settings presents radio choices in this order: Built-in AI via API, then Bring Your Own AI via Subscription. The macOS-style circles retain native input semantics, keyboard navigation and visible focus; forced-colors mode uses browser rendering. aiMode defaults to subscription, without a default suffix on either label; explicit saved choices are preserved. aiWebsite independently selects ChatGPT or Claude, defaulting to ChatGPT. Service choices likewise have no default suffixes. Both modes share aiEnabled, explanation language, optional nearby context and the neutral sparkle/selection entry. Switching methods cancels obsolete work and clears the reader’s local passage conversation; switching back preserves encrypted keys and API model choices. The API product rejects subscription-mode requests before reading a key or making a provider call.
Customs Province owns ai-website.js. The fixed handoff destinations are https://chatgpt.com/?q=... and https://claude.ai/new?q=.... core/ai-web.js builds a single user-voiced question from the shared explanation builder and bounded JSON-quoted document data: “I selected the text below while reading. Please help me understand it…”. The builder handles transport-specific wording directly, without rewriting an API system prompt through string replacement. An encoded URL budget of 12,000 characters is a conservative extension policy, not a website limit: over-budget questions remain complete in a sandbox-local Copy question/Open AI website panel. No partial question is sent; the separate Open choice omits q and the user pastes the question manually. Copy uses the existing write-only sandbox policy and never sends a clipboard command through the host. Neither website’s Submit button is clicked; prefill/automatic submission behavior is owned by that website.
Website mode uses the browser’s ordinary logged-in page, including free or subscribed accounts. It does not require or verify a paid subscription, inspect cookies/credentials, inject a content script, read answers, operate model selectors or return website replies into Cosmic PDF. No iframe embedding, proxy or removal of website security headers is used. This ordinary user message has no API system-role authority; prompt instructions are a mitigation, not a guarantee that the website model ignores malicious document text or that its own tools are disabled. The extension itself provides no model tools/actions.
The trusted host routes bounded AI_OPEN_WEBSITE/AI_CANCEL_WEBSITE requests. The background derives the owner tab from the sender, validates the extension ID/frame/path, then rechecks the current document ID/URL through runtime.getContexts before navigation. Public methods never accept an arbitrary destination or tab ID. Global serialization prevents rapid opens from creating competing companions. Preference changes, cancellation and source departure retire pending jobs. An already opened conversation is not automatically closed when its PDF closes or when AI is disabled.
Only the source-reader tab ID and owned companion-tab ID live in a bounded storage.session registry (128 entries); it survives worker suspension, not browser exit. The registry is cached after one lazy read and written only when ownership changes. It contains no document/question URL, filename, text or account data. Reuse requires a still-live companion in the same window/privacy context and one of the exact supported website origins. Closed, repurposed, moved or unrelated-split tabs are left alone and replaced. Source departure/closure and companion departure/closure retire registry entries. Reuse navigates the companion to a new question, so it can leave a previous conversation or replace an unsent draft; this is described in Settings.
Feature detection uses tabs.createSplit where exposed (officially Chrome 155+), with matching pinned/group state, adjacency and no unrelated existing split. An already paired PDF/AI split is retained. Unavailable/rejected automatic splits leave a functioning neighboring tab without a duplicate window/tab or warning. No split-width API is exposed, so the extension does not claim a 70:30 ratio or simulate browser UI controls. Chrome Tabs API documents the boundary. No new extension permission is needed. Website-mode prefill text necessarily enters an AI website URL and can appear in history/sync or website retention; unlike API mode, conversation lifetime is controlled by that website.
python3 scripts/build-downloads.py builds deterministic installation and full-source ZIPs from the current tracked working-tree files. Stage new source files before building. Fixed timestamps and regular-file permissions keep archives reproducible. The installation archive includes only extension/; the source archive includes tracked project sources except generated downloads, local/ignored folders and personal helpers. Neither archive includes Git metadata or g.sh. No downloaded package recursively includes generated ZIPs. SHA-256 metadata is written next to the files.
Commit/push downloads/ with each release. The existing main-root Pages site at cpdf.songming.org serves the ZIPs directly. There is no GitHub Actions workflow, token requirement, redirect to a GitHub download host or website-side build step. The marketing site is maintained separately at pdf.songming.org.