Skip to content

Latest commit

 

History

History
803 lines (663 loc) · 70 KB

File metadata and controls

803 lines (663 loc) · 70 KB

LW Power Reader - Detailed Specification

Overview

LW Power Reader is a userscript that provides an enhanced interface for reading recent comments on LessWrong (and potentially EA Forum). It replaces the native /reader page with a custom UI optimized for high-volume comment consumption.

CRITICAL ENFORCEMENT POLICY: Never remove or downgrade a feature described in this document without explicit USER approval. If any discrepancy is observed between this SPEC and the code, either warn the USER immediately or fix the code to match the SPEC, depending on severity and impact on the user experience. TAG NOTE: Spec tag numbers do not have to be consecutive. Prioritize keeping existing tag IDs stable because tests use these tags to verify coverage.

Target URLs

  • [PR-URL-01] https://www.lesswrong.com/reader - Main reader interface
  • [PR-URL-02] https://www.lesswrong.com/reader/reset - Clear all storage and show setup UI
  • [PR-URL-03] https://forum.effectivealtruism.org/reader

Route Behavior:

  • [PR-URL-04] /reader - If loadFrom is set, load comments from that date. If empty, show setup UI.
  • [PR-URL-05] /reader/reset - Clear all local persisted state (reader state plus sync metadata/settings), then run reset-route sync replay and redirect to /reader.

Core Architecture

Page Takeover Strategy ("Nuclear Option")

[PR-ARCH-01] Since LessWrong is a React-heavy SPA, the userscript must completely take over the page to prevent conflicts:

  1. [PR-ARCH-02] Run at document-start - Execute before any site scripts load
  2. [PR-ARCH-03] window.stop() - Halt all pending resource loading
  3. [PR-ARCH-04] Script blocking - MutationObserver removes any dynamically added <script> elements
  4. [PR-ARCH-05] DOM replacement - Clear document.documentElement.innerHTML and inject custom UI
  5. [PR-ARCH-06] Protection observer - Re-inject UI if site code attempts to clear it

Data Source

  • [PR-DATA-01] API: LessWrong GraphQL endpoint (/graphql)
  • [PR-DATA-02] Transport: GM_xmlhttpRequest for cross-origin requests
  • [PR-DATA-03] Strict-By-Default Errors: The GraphQL client MUST throw on GraphQL errors by default, including responses that contain both data and errors. This protects mutation and interactive workflows from silent failure.
  • [PR-DATA-03.1] Scoped Partial Success: Partial-success mode is allowed only via explicit opt-in per call site. In partial mode, the client MUST return data only when all errors match an allowlist of tolerated patterns; otherwise it MUST throw.
  • [PR-DATA-03.2] Bulk-Read Tolerance Only: Tolerated partial-success behavior is limited to bulk read paths (archive fetches and recent-comments list/polling fetches). Interactive and mutation paths (votes, reactions, navigation actions) MUST remain strict.
  • [PR-DATA-04] UI Fallbacks: Rendering components MUST handle missing or null field values (like pageUrl) gracefully, providing sensible fallbacks (e.g., '#' for links) to prevent UI crashes or broken interactions when the server fails to resolve specific fields.
  • [PR-DATA-05] Cross-Site Query Compatibility: Queries are written in the modern selector/top-level-args syntax (for codegen type safety on LW). A runtime adapter in the GraphQL client automatically rewrites them to the legacy input/terms syntax when running on EA Forum. Every query using selector MUST be registered in LEGACY_ADAPTERS (src/shared/graphql/legacyAdapter.ts) so it works on EAF.
  • [PR-DATA-06] Server-Sanitized Content Contract: Forum HTML fields from LW/EAF GraphQL resolvers are treated as server-sanitized content, including post/comment htmlBody, user htmlBio, and revision/tag htmlHighlight (for previews such as wiki/tag descriptions).
  • [PR-DATA-06.1] Direct API HTML Rendering: Client rendering of these API-provided forum HTML fields is pass-through (no client sanitizer layer). Security for these fields depends on server-side sanitization in forum resolvers.

Reader Persistence Sync (Firestore)

  • [PR-SYNC-01] Sync defaults to enabled for authenticated users and is scoped per site (lw/eaf) so data never cross-pollinates between forums.
  • [PR-SYNC-02] Sync identity is derived from currentUser._id plus currentUser.abTestOverrides.pr_sync_secret_v1; when missing, the script bootstraps the secret via updateUser.
  • [PR-SYNC-02.1] Security posture for v3 is an accepted convenience tradeoff: abTestOverrides is treated as a capability-token store for low-sensitivity sync state, not a hardened secret vault. If the forum session, page JS context, privileged forum tooling, or browser extension environment is compromised, sync node derivation and remote sync state can be compromised too.
  • [PR-SYNC-03] Remote state is stored in Firestore at pr_sync_v1/{site}/nodes/{syncNode} using REST documents:commit with single-write CAS preconditions (exists=false or updateTime).
  • [PR-SYNC-04] Synced subset in v3: read, loadFrom, authorPrefs, and aiStudioPrefix. read/loadFrom/authorPrefs use clear-epoch barriers for durable reset/overflow clears; aiStudioPrefix uses versioned last-write-wins merge.
  • [PR-SYNC-05] /reader/reset clears local syncable fields immediately and attempts an authoritative remote clear; if identity/secret is unavailable, a pending-reset marker is persisted for replay.
  • [PR-PERSIST-102] Session-advance writes (loadFrom advancement and related read-state cleanup) request an immediate sync flush scheduling pass, while still honoring push-floor, quota, and local-budget gates.

Persistence Plan v3 Requirement IDs (PR-PERSIST-*)

  • PR-PERSIST-01: deterministic sync node derivation
  • PR-PERSIST-02: external KV storage (Firestore document backend)
  • [PR-PERSIST-03]: site-scoped sync
  • [PR-PERSIST-04]: startup hydrate before setup gate
  • [PR-PERSIST-05]: reset clears local state + writes remote cleared envelope
  • [PR-PERSIST-06]: synced fields (read, loadFrom, authorPrefs, aiStudioPrefix)
  • [PR-PERSIST-07]: anonymous/missing-identity guard
  • PR-PERSIST-08: loadFrom override logging
  • PR-PERSIST-09: Firestore conflict handling (failed updateTime precondition path)
  • PR-PERSIST-10: setLoadFrom pure + explicit clear helper semantics
  • PR-PERSIST-11: identity fallback uses user-gated lastSyncNode probe for diagnostics only (no copy-forward writes)
  • PR-PERSIST-12: unknown-higher-schema read-only mode (including mid-CAS-retry re-read detection)
  • [PR-PERSIST-13]: strict envelope validation / corruption handling (invalid remote => local-only push-disabled session mode, reset still allowed)
  • [PR-PERSIST-14]: read-only throttling (default 45s, allowed 30-60s)
  • [PR-PERSIST-15]: one-time sync-secret bootstrap and verification in abTestOverrides
  • PR-PERSIST-16: late startup sync apply with visible non-blocking notice
  • [PR-PERSIST-17]: sync toggle (power-reader-sync-enabled) UX + behavior
  • PR-PERSIST-18: export/import dirty-mark + sync interaction semantics
  • [PR-PERSIST-19]: read-overflow guard (local read clear + read.clearEpoch propagation)
  • PR-PERSIST-20: authorPrefs overflow compaction (oldest v:0 first, then oldest overall if required)
  • PR-PERSIST-21: pull-on-focus + fallback periodic pull with cross-tab coalescing, with flush deferral while pull is in-flight
  • PR-PERSIST-22: clear-epoch barriers for read, loadFrom, authorPrefs
  • [PR-PERSIST-23]: dirty-gated authorPrefs merge on write plus deterministic loadFrom merge resolution
  • [PR-PERSIST-24]: identity permission fail-closed (no retry storms)
  • [PR-PERSIST-25]: reset uses CAS loop and monotonic clear-epoch advancement under conflict
  • PR-PERSIST-26: conflict coalescing/backoff for repeated updateTime precondition failures
  • [PR-PERSIST-27]: Firestore rules enforce structure/caps while client validation enforces dynamic-leaf value shapes
  • PR-PERSIST-28: loadFrom.version lifecycle (init/increment/reset semantics)
  • PR-PERSIST-29: Firestore server-time diagnostics path supports optional lastPushedAtMs
  • PR-PERSIST-30: reset-failure durability via persisted barriers + pendingRemoteReset
  • PR-PERSIST-31: post-merge cap enforcement on all write paths (including CAS retries)
  • PR-PERSIST-32: optional loadFrom.value validation support for clear-barrier-only states
  • PR-PERSIST-33: lastSyncNode persisted only after secret finalization and confirmed current-node validity
  • [PR-PERSIST-34]: fallback-node probe is user-gated (lastUserId match) and blocked while pendingRemoteReset is active
  • [PR-PERSIST-35]: user-switch clears fallback/reset pointers (lastSyncNode, pendingRemoteReset, pendingRemoteResetAt, pendingRemoteResetTargets) and runtime CAS cache
  • PR-PERSIST-36: Firestore rules allow only exact doc-path get/create/update; list denied
  • PR-PERSIST-37: top-level allowlists in Firestore rules with client-side dynamic-entry validation fallback
  • [PR-PERSIST-38]: optional lastPushedAtMs acceptance in schema + rules
  • PR-PERSIST-39: idempotent pending-reset replay via stored target clear epochs
  • PR-PERSIST-40: loadFrom.value null-normalized-to-absent and bounded sentinel/ISO validation
  • PR-PERSIST-41: required-child enforcement for read, loadFrom, authorPrefs objects in Firestore rules
  • PR-PERSIST-42: delete unsupported in v3 Firestore rules (reset writes cleared envelope)
  • PR-PERSIST-43: sync node derivation uses lowercase hex encoding
  • [PR-PERSIST-44]: Firestore document path contract (pr_sync_v1/{site}/nodes/{syncNode})
  • PR-PERSIST-45: concurrency via Firestore REST currentDocument.updateTime precondition
  • PR-PERSIST-46: Firestore rules enforce read.value.size <= 10000 and authorPrefs.value.size <= 1000
  • PR-PERSIST-47: deny Firestore list to prevent parent enumeration
  • PR-PERSIST-48: required TTL retention via bounded expiresAt policy
  • PR-PERSIST-49: no client quota introspection; quota handling is error-driven (429/RESOURCE_EXHAUSTED)
  • PR-PERSIST-50: quota-limited circuit breaker persists cooldown and probe schedule
  • PR-PERSIST-51: quota-limited mode preserves dirty state and pending reset barriers (no data loss)
  • PR-PERSIST-52: cross-tab quota cooldown coordination via shared retry timestamp key
  • PR-PERSIST-53: per-user/day local push self-budget with soft/hard thresholds
  • PR-PERSIST-54: hidden/idle gating suspends non-critical sync activity
  • PR-PERSIST-55: post-recovery ramp-up avoids immediate full-rate burst
  • [PR-PERSIST-56]: UI exposes quota-limited and local-budget-limited states with retry ETA
  • PR-PERSIST-57: Firestore Emulator integration suite validates REST path end-to-end without production dependencies
  • PR-PERSIST-58: Emulator tests validate deployed rules behavior (path constraints, list deny, schema/map-size enforcement)
  • PR-PERSIST-59: Emulator tests validate currentDocument.updateTime CAS precondition conflict/retry behavior
  • PR-PERSIST-60: Integration tests enforce emulator-only endpoints (fail closed on production Firestore host)
  • PR-PERSIST-61: write protocol uses single-write documents:commit payloads only
  • PR-PERSIST-62: create-if-missing uses exists=false precondition with raced-creator re-read fallback
  • PR-PERSIST-63: bootstrap lock uses ownership token + expiry and stale-lock reclaim
  • PR-PERSIST-64: bootstrap lock prefers navigator.locks with localStorage fallback
  • PR-PERSIST-65: pending reset replay is prioritized over late pull-apply when reset is unresolved
  • PR-PERSIST-66: startup recovery marks syncable fields dirty when local sync meta is missing/invalid/inconsistent
  • PR-PERSIST-67: Firestore index overrides disable indexing for large dynamic maps (fields.read.value, fields.authorPrefs.value)
  • PR-PERSIST-68: incident response path supports temporary write-disable and API-key/TTL operational controls
  • PR-PERSIST-69: expiresAt is required and bounded by rules (request.time < expiresAt < request.time + 181d, where 180d target + 1d skew headroom)
  • PR-PERSIST-70: Firestore REST writer uses explicit typed-value serialization/deserialization
  • PR-PERSIST-71: CAS conflict detection handles Firestore contention/precondition variants (not single-code only)
  • PR-PERSIST-72: REST requests use bounded timeout, malformed commit token responses are treated as uncertain outcomes, and in-flight state always clears in finally
  • PR-PERSIST-73: missing doc (404 NOT_FOUND) is treated as empty remote envelope
  • [PR-PERSIST-74]: non-reset write cadence enforces per-node minimum interval with dirty coalescing
  • PR-PERSIST-75: best-effort cross-tab push coalescing uses shared last-push timestamp key
  • PR-PERSIST-76: invalid remote envelope enters local-only push-disabled mode for session
  • PR-PERSIST-77: long-lived pending reset with missing secret surfaces stronger user-facing status hint
  • PR-PERSIST-78: CAS update token (updateTime) is treated as opaque exact server string (no parse/reformat)
  • [PR-PERSIST-79]: rules bound clearEpoch/version integers to sane upper limits and client validates bounds
  • [PR-PERSIST-80]: missing/blocked Firestore userscript connect permission surfaces explicit sync status hint
  • PR-PERSIST-81: reset CAS writes include fresh bounded expiresAt and standard diagnostics fields
  • PR-PERSIST-82: dynamic-key hygiene enforces local key length/charset bounds before merge/write
  • [PR-PERSIST-83]: envelope timestamps (expiresAt, diagnostics) prefer server-time anchors; client-time fallback is first-write-only
  • [PR-PERSIST-84]: backend decode tolerates malformed dynamic author-preference leaf entries without crashing sync startup
  • [PR-PERSIST-85]: sync counter increments (loadFrom.version, authorPrefs.*.version) are overflow-safe and clamp at configured max
  • [PR-PERSIST-86]: uncertain write outcomes reconcile via authoritative re-read before retry and preserve dirty state on failure
  • [PR-PERSIST-87]: Firestore runtime config is not exported on window globals and host parsing rejects invalid URL-style hosts
  • [PR-PERSIST-88]: deep stable-compare utility is order-insensitive for nested maps used in merge/diff checks
  • [PR-PERSIST-89]: device id normalization is bounded so writer labels remain Firestore-rule-safe
  • [PR-PERSIST-90]: GraphQL client forwards explicit operationName when provided in request options
  • [PR-PERSIST-91]: reset clear-all path removes sync metadata/settings keys in addition to reader field storage
  • [PR-PERSIST-92]: preview open timers must re-check hover intent and visibility before rendering the preview
  • [PR-PERSIST-93]: TTL anchor computation clamps stale server anchors and does not fall back to remote diagnostic timestamps
  • [PR-PERSIST-94]: dirty local loadFrom clears (empty-string intent) MUST advance loadFrom.clearEpoch so stale remote values cannot be resurrected on merge/reload
  • [PR-PERSIST-95]: sync storage emits an "applied" event channel distinct from dirty-change tracking; silent applies still notify the applied channel
  • [PR-PERSIST-96]: cross-tab sync watchers are apply-only (no GM write-back) and support manager compatibility via listener feature-detection plus polling fallback
  • [PR-PERSIST-97]: UI consistency layer hot-patches read/author visual state without structural re-render and bounds patch work to 50 nodes per frame
  • [PR-PERSIST-98]: external read/loadFrom applies update in-memory read/loadFrom caches directly (no forced GM re-read)
  • [PR-PERSIST-99]: idle/visibility resume pull requests route through existing performPullAndMerge() gating (quota + cross-tab lastPull)
  • [PR-PERSIST-100]: hot-patch timestamps prefer data-posted-at-ms, fallback to time[datetime], and avoid unread downgrades when timestamps are missing/invalid
  • [PR-PERSIST-101]: successful CAS writes must clear dirty flags only for fields unchanged since commit start; local edits that occur during an in-flight commit remain dirty for the next flush
  • [PR-PERSIST-102]: session-advance applies trigger throttled priority flush scheduling (delay=0) without bypassing push-floor/quota/local-budget guards

Features

1. Content Loading (Comments & Posts)

Parameter Value Notes
Default load count 800 max Configurable via loadMax
Load method Range-Bound Sequential Fetch Fetches comments first, then posts in same window
Starting point loadFrom datetime Persisted ISO date string
Processing Unified Stream Merges posts and comments into post-centric groups

[PR-POST-01] Post Header Layout:

  • Author Preference Arrows: ↓ / ↑ for quick liking/disliking of authors.
  • Metadata: Author name (clickable link to profile), timestamp (link to original post).
  • Vote Controls: Karma vote buttons for posts and comments. Agreement axis buttons/scores are comment-only; post-level agreement is represented via EAF reaction chips.
  • Action Buttons: [e] (expand), [a] (load all), [c] (scroll to comments), [n] (next post).
  • Structural Toggles: [−] (collapse), [+] (expand).

Data Flow:

  1. [PR-LOAD-01] Initial Fetch: User info, comments, and current user data are fetched in parallel.

    • Comments use allRecentComments view.
    • If loadFrom is set: use sortBy: "oldest" to load forward.
    • If loadFrom is __LOAD_RECENT__: use sortBy: "newest" to get the latest batch.
    • [PR-LOAD-01.1] Initial loadFrom Snapshot (Recent Mode): If startup began with loadFrom = __LOAD_RECENT__, immediately after the initial comments batch is fetched, set loadFrom to the datetime of the oldest fetched comment. If startup began from an explicit datetime, preserve it unchanged.
  2. [PR-LOAD-02] Content Enrichment: Before rendering, additional data is fetched:

    • Post Bodies: Full post content for the post batch is fetched via GraphQL (new selector), including body fields (htmlBody/contents).
    • Header-Only Exception: Full bodies are fetched only for posts in the current post window; header-only posts are created only for out-of-window posts referenced by comments and their bodies load on-demand.
    • Post Batch Ordering: The server currently returns the new post batch newest-first.
    • Post Date Range: Posts are fetched to match the date window of the fetched comments.
      • If moreCommentsAvailable is true (comments hit loadMax), fetch posts from loadFrom up to the newest fetched comment.
      • If moreCommentsAvailable is false, fetch all posts newer than the oldest fetched comment (open-ended to "now").
      • If loadFrom is __LOAD_RECENT__, treat the lower bound as the oldest fetched comment (the sentinel is not a real timestamp).
    • Ordering Detail: The post date window is derived from the comment batch timestamps (oldest/newest) captured for this load, not from any later loadFrom updates.
    • Parent Placeholder Chain: Comments include nested parentComment fields (multi-level), which are used to build a chain of parent placeholders with IDs (no extra fetch required in this phase).
    • Subscriptions: Author favor/subscription lists.
  3. [PR-LOAD-03] Single Render: UI renders once with all data available. Status messages update in the header during loading phases.

    • moreCommentsAvailable: Set true only when the comments batch hits loadMax, indicating the server likely has more comments in this window. This flag shows the warning banner and can gate smart loading.
    • __LOAD_RECENT__ Mode: When loadFrom is __LOAD_RECENT__, set moreCommentsAvailable = false to avoid paging in recent-mode.
  4. [PR-LOAD-04] Items in the feed (posts and comment groups) are sorted by Tree-Karma descending (see Section 25).

    • Tie-breaker: Use postedAt (newest-first).
  5. [PR-LOAD-05] Track read state locally with { [itemId]: 1 } dictionary.

  6. [PR-LOAD-06] Smart Loading (Reply-Focused Heuristic):

    • Deferred Phase: Smart loading runs after enrichment but before the initial render, as a separate phase.
    • Gating: Skip smart loading unless moreCommentsAvailable is true (or a test override forces it).
    • Reply-Focused Strategy:
      • Very High Activity Threads (>= 3 unread comments with missing children in the same thread): Fetches the full thread (GET_THREAD_COMMENTS).
      • All other unread comments with missing children: Fetches direct replies only for each unread comment that has descendants.
      • Missing Children Check: Uses directChildrenCount vs loaded children count to detect missing descendants.
    • Parent Context: Missing parents are represented by placeholders; full parent loading happens on-demand via [t] or [^].
    • Parallel Fetching: Thread and reply fetches run concurrently.

First-Time Setup Flow:

[PR-SETUP-01] On first use (or after /reader/reset), display a setup prompt with:

  • [PR-SETUP-02] A calendar date picker to select loadFrom date
  • [PR-SETUP-03] If left empty/confirmed without selection: load most recent 800 comments
  • [PR-SETUP-04] Otherwise, selected date is stored in loadFrom as ISO 8601 string (start of day in user's timezone)

Continuation/Pagination:

  • [PR-LOAD-09] When user scrolls to bottom of page, update loadFrom to the ISO timestamp of the newest comment from the initial comments batch + 1ms (excluding smart-load and user-triggered loads).
  • [PR-LOAD-10] Store via GM_setValue('power-reader-read-from', isoDateString)
  • [PR-LOAD-11] When moreCommentsAvailable is true (comments batch hit loadMax), show warning: "There are more comments available. Please reload after reading current comments to continue."
  • [PR-LOAD-12] Recent Comments Partial-Error Resilience: Initial recent-comments fetch and bottom "check for more comments" fetch may accept partial GraphQL data only when every error matches the tolerated missing-comment-document/pageUrl resolver patterns; otherwise they must fail fast.

2. Post & Comment Threading

Items are organized post-centrically:

  1. [PR-THREAD-01] Comments are grouped under their parent Post. If a comment's post was not in the posts fetch, a minimalist post header is still created.
  2. [PR-THREAD-02] Post-Comment Integrity: Comments always render under their associated post regardless of fetch order.

Post Display:

  • [PR-POST-02] Unified Metadata: Posts now use the same one-line metadata layout as comments (Author, Score, Reactions, Timestamp).
  • [PR-POST-03] Header-Only Definition: Header-only posts are created when a comment carries a post reference outside the current post load window; they exist only to anchor comments.
  • [PR-POST-06] Conditional Controls: Voting buttons are hidden for header-only posts or collapsed views to prevent voting before content is loaded. The karma score MUST remain visible; agreement score visibility applies only where an agreement axis exists (comment metadata).
  • [PR-POST-07] Inline Loading: Clicking the title of a header-only post (data-action="load-post") fetches the content via GraphQL and replaces the header with a full post render inside the current feed.
  • [PR-POST-04] Truncation: Posts taller than 50vh are truncated with a "Read More" button and gradient overlay.
  • [PR-POST-05] Expansion: "Read More" expands the post to full height in-place.
  • [PR-POST-11] Smart Truncation Handling: If a post is technically "truncated" (has the class) but its content fits within the allotted height, the "Read More" overlay MUST be hidden automatically, and the [e] button disabled with a descriptive tooltip ("Post fits within viewport without truncation"). If the post is already expanded and still small enough that toggling is unnecessary, [e] may also be disabled with "Post body is small and doesn't need toggle".

Read Tracking for Posts:

  • [PR-POST-08] Body-Centric Marking: Posts are marked as read when the bottom of their body content (.pr-post-content) is scrolled past, rather than the entire post-plus-comments container. If the body is collapsed or missing (header-only), the header bottom is used as the threshold instead.
  • [PR-POST-09] Read Style: Greyed out title and body opacity reduced to 0.8.
  • [PR-POST-10] Counter: Included in the "Unread Items" count in the status bar.
  • [PR-POST-12] Fully Read Filter: If a post is marked as "read" (explicitly or via timestamp) AND it has no unread comments, it MUST NOT be rendered. This keeps the feed clean of fully consumed threads.

3. Comment Threading (Nested)

Comments under a post are organized hierarchically:

Collapse behavior:

  • [PR-NEST-01] [−] button to recursively collapse an item and all its children. Collapsing any item MUST reveal a [+] (Expand) button, regardless of whether that item has children.
  • [PR-NEST-02] Post Collapse: Collapsing a post hides both the post body (.pr-post-content) and all associated comments.
  • [PR-NEST-03] Hover over [+] or a collapsed placeholder to preview content.
  • [PR-NEST-05] Interactive Thread Lines: The left border of comment replies (.pr-replies) acts as a recursive toggle. Clicking the indentation line (or its 24px hit area) toggles the collapsed state of the parent comment. The line MUST darken and show a subtle background highlight on hover to indicate interactivity.

Placeholder Logic

  • [PR-NEST-04] Missing Parent Placeholders: If a loaded comment references a parent comment that is not loaded, render an empty placeholder comment (no header or body) with a read-style border. This check must be re-applied any time comments are loaded or re-rendered.
  • [PR-NEST-04.1] Parent Chain via Nested Fields: Use the nested parentComment fields returned by the server to build a chain of placeholder parents with correct IDs (multi-level), without additional fetches.
  • [PR-NEST-04.2] Depth Limit: The placeholder chain only extends as far as the nested parentComment fields in the query; deeper parents require on-demand fetch via [t] or [^].
  • Thread Integrity With Placeholders: Thread structure must remain correct whether or not placeholders exist. Replies should nest under their true parent if loaded, or under the placeholder if the parent is missing.
  • [PR-NEST-06] Ancestor Continuity: The rendering system MUST preserve the full ancestor chain in the DOM for every unread or context comment, even if those ancestors are marked as "read". This prevents broken threads and ensures that "Trace to Root" can always reach the top without forced server re-fetches for already loaded data.

4. Parent Navigation

  • [PR-NAV-01] [^] button next to each comment. Always present, including for top-level comments.
  • Hover:
    • Highlighing & Previews:
    • [PR-NAV-02] Highlighting:
      • [PR-NAV-02.1] Comments: If the parent is a comment and it exists in the DOM, applying the highlight (.pr-parent-hover, #ffe066) to the comment element (.pr-comment).
      • [PR-NAV-02.2] Posts: If the parent is a post, the highlight applies to both the post header (.pr-post-header) and the post body (.pr-post-body-container) if loaded.
      • [PR-NAV-02.3] Sticky Sync: If the sticky header is visible for the target post, it must also receive the highlight simultaneously with the main content.
      • [PR-NAV-02.4] General Rule: The highlight applies regardless of visibility and takes precedence over score and recency colors.
    • [PR-NAV-03] Sample-Based Visibility: Visibility detection for parent navigation MUST use document.elementFromPoint() to verify if the target is actually reachable/visible to the user, correctly accounting for overlaps from the Sticky Header, AI Box, or other floating UI without hardcoded offsets.
    • [PR-NAV-04] If the target is obscured or partially off-screen (the sample check fails), the preview popup is shown.
    • [PR-NAV-05] Targets inside the sticky header are considered partially visible (header only) and thus trigger a preview for the full content.
    • [PR-NAV-09] Previewing: If the target is off-screen, partially hidden, collapsed, or its content is not yet loaded ("unloaded"), it shows a preview popup instead.
  • Click: Scrolls to parent comment or the Post Header.
    • [PR-NAV-06] Smart Scroll: Post parents scroll to the top of the viewport (block: 'start'). Comment parents scroll to just below the Sticky Header of the destination post (accounting for its dynamic height), but only when needed.
    • [PR-NAV-06.1] Viewport Stability on Re-render: If parent navigation triggers a re-render (e.g., expanding a placeholder or deep-loading a missing parent), preserve the focal comment's viewport position first; then scroll only if the resolved parent is still not fully visible.
    • [PR-NAV-07] Highlight: Provides a temporary animated flash highlight (.pr-highlight-parent).
    • [PR-NAV-08] Deep Loading: If a parent comment is not present in the current feed, clicking [^] fetches it from the server and injects it into the post group as context.
    • [PR-NAV-10] Post-Rerender Event Reattachment: After a post group is re-rendered (e.g., due to deep loading), hover previews and parent highlighting must be re-initialized on the new DOM elements.
  • [PR-NAV-11] Automatic Expansion: When navigating via [^] (Find Parent) or [t] (Trace to Root), if any ancestors (including the target) are currently rendered as "read placeholders", the system MUST automatically mark them as forceVisible and re-render the post group to expand them. This provides the user with the full context of the thread they navigated to.

5. Read State Tracking

Storage: GM_setValue / GM_getValue (persisted across sessions)

  • Marking logic:
  • [PR-READ-01] Item-Centric Threshold: Items are marked read when their specific content body is scrolled past, rather than their entire bounding box (which might include long nested subthreads).
    • Posts: Marked when the bottom of .pr-post-content is passed. (See [PR-POST-08]).
    • Comments: Marked when the bottom of .pr-comment-body is passed. If a comment is collapsed, the bottom of the .pr-comment-meta (header) is used as the threshold.
  • [PR-READ-02] Bottom of Page Logic: Bottom detection uses a small tolerance margin (currently ~150px from document end) for cross-browser and zoom robustness. Once in this bottom zone, all currently visible unread comments become eligible for read-marking (using the same delay as the normal scroll-past threshold).
  • [PR-READ-03] Session Advancement: When the "Bottom of Page" condition is met, update the stored loadFrom value to the ISO timestamp of the newest comment from the initial comments batch PLUS 1 millisecond (excluding smart-load and user-triggered loads). Advancement triggers unconditionally at bottom-of-page (no additional check on unread count). Startup exception: if unread count is already zero on initial render and comments are loaded, this same advancement flow may run immediately.
  • [PR-READ-04] 2-second delay before marking - uses setTimeout to avoid marking during fast scrolling.
  • [PR-READ-05] Unread Persistence: On page refresh, only unread items are shown with full content. Read comments with fewer than 2 unread descendants are collapsed into minimal placeholder bars (preserving thread structure context); read comments with 2+ unread descendants are rendered in full with greyed-out styling. Placeholders can be expanded on click, or automatically when navigated to via [^] or [t].
  • [PR-READ-06] Cleanup: During session advancement (bottom-of-page processing), read IDs with known postedAt older than the new loadFrom are removed. IDs not found in the current loaded set are preserved to avoid cross-tab/session data loss.
  • [PR-READ-07] Implicit Read State (Temporal Boundary): Any item (Post or Comment) with a postedAt timestamp older than the current loadFrom value is treated as Read by default, even if its ID is not present in the explicit readState storage. This ensures that old posts anchoring new comments are correctly greyed out and excluded from unread counts.

6. Author Information & Preferences

[PR-AUTH-03] Full Name Display: Authors are now displayed using their displayName (full name) instead of their username (short name/handle) whenever available. [PR-AUTH-04] Profile Links: Author names are clickable links that open the user's profile (/users/slug) in a new tab. [PR-AUTH-09] Hover Previews: Hovering over an author's name shows a preview popup containing:

  • Display Name and Username
  • Total Karma
  • HTML Bio/Description (excerpt)

Users can also mark authors as favored or disfavored:

Action UI Effect
[PR-AUTH-01] Favor Click [↑] next to author Comments highlighted (e.g., green tint)
[PR-AUTH-02] Disfavor Click [↓] next to author Comments de-emphasized or hidden

7. Visual Indicators

Color Coding

Indicator Color Meaning
[PR-VIS-01] Comment points Pink gradient Higher normalized score = more pink header
[PR-VIS-02] Post points Purple gradient Higher normalized score = more purple header
[PR-VIS-03] Recency Yellow gradient More recent comments = more yellow body
[PR-VIS-04] Replies to you Green border Comments replying to you (2px unread, 1px read)
[PR-VIS-05] Read items Grey text Already viewed posts or comments
[PR-VIS-06] Normal items Black border Default state
[PR-VIS-07] Rejected/Junk Thin red border Moderated or rejected comments; collapsed by default

7.2 Content Formatting

  • [PR-VIS-08] Blockquotes: Stylized with a distinct left border (solid 3px #e0e0e0).
  • [PR-VIS-09] Footnotes: Footnote back-links (^) and content display inline.

Normalization Algorithm

Baseline: karma at author's voting power = no color (neutral).

[PR-VIS-11] Age-based expected points threshold:

const base = 5 + 2 * Math.sqrt(ageHours);
pub = isPost ? base * 6.7 : base;

[PR-VIS-12] Normalized score calculation:

normalized = (points - plb) / (pub - plb);
if (authorPreferences[author]) 
    normalized += authorPreferences[author] * 0.52;

[PR-VIS-13] Auto-hide threshold:

if (normalized < -0.51) HideItem(x);

Dynamic Font Size

  • [PR-VIS-10] Higher karma comments and posts get larger header font (up to 150%).
  • [PR-VIS-14] Unified Utility Buttons: All text-based right-side buttons ([e], [a], [^], etc.) use a unified .text-btn class with a constant 13px font size and specific opacities: 0.8 (enabled), 1.0 (hover), 0.15 (disabled).
  • [PR-VIS-15] Reaction Scaling: Unlike control buttons, reaction icons and chips MUST use relative units (em) to scale proportionally with the dynamic font size of the parent item.

8. Date/Time Display

  • [PR-DATE-01] Convert all timestamps from UTC to user's local timezone.
  • [PR-DATE-02] Click on timestamp: Always opens the original item permalink in a new tab.

9. Status Line Display

The status line at the top of the reader provides a summary of the current session and loaded data.

  • [PR-STATUS-01] Layout: Displays date range, unread count, comment breakdown, post count, and user information in a single row with emoji icons.
  • [PR-STATUS-02] Date Range: Shows the range from the loadFrom starting point to the newest comment in the initial batch.
  • [PR-STATUS-03] Comment Breakdown: Shows total comments loaded, with a breakdown of new (unread), context (read but shown for structure), and hidden (read/collapsed).
  • [PR-STATUS-04] Post Count: Shows the number of visible post groups and an indicator if any posts were filtered out (fully read).
  • [PR-STATUS-05] Logged-in User: Displays the username of the current authenticated user or a "not logged in" indicator.
  • [PR-STATUS-06] Sync Status: Displays current reader-sync mode (on, off, local-only, syncing, quota-limited, etc.) in the main status row.

10. Link Behavior

  • [PR-LINK-01] All external links automatically get target="_blank".
  • [PR-LINK-02] Internal anchor links (#...) are preserved for in-page navigation.
  • [PR-LINK-03] Links to /reader are kept as same-tab navigation.

11. Sticky Post Header

  • [PR-STICKY-01] When the post header scrolls above viewport but comments are visible, a sticky header appears.
  • [PR-STICKY-02] Only one sticky header shows at a time.
  • [PR-STICKY-03] Sticky header matches the style of the original header.
  • [PR-STICKY-04] Post Navigation: Clicking the title or empty areas in the sticky header scrolls the page back to the original post header.
  • [PR-STICKY-05] Hover on title or header: Shows a preview of the post content (since body content is usually not visible in sticky mode).
  • [PR-STICKY-06] External Navigation: Click the date/timestamp to open the post page in a new tab.
  • [PR-STICKY-07] Collapse/Expand: Buttons synchronize state with original comments.
  • [PR-STICKY-08] Interactive Elements: Voting buttons, reaction chips, and other interactive elements in the sticky header work normally without triggering the scroll-to-original-post behavior.

12. Post Header Action Buttons

Action buttons are displayed in the post header (both sticky and regular), positioned to the left of the existing collapse [−]/[+] buttons.

[PR-POSTBTN-01] Load/Expand Post Body ([e]):

  • If the post body is not yet loaded (header-only post):
    • Fetches the full post content from the server.
    • Renders the post body in expanded mode.
    • Button shows [...] while loading.
  • If the post body is already loaded:
    • Toggles between truncated (50vh max-height with gradient overlay) and fully expanded (no truncation).
  • Disabled State: Disabled (0.15 opacity, no hover) if the post body is fully loaded AND its actual content height is less than the truncation threshold (fits within viewport without truncation). This is verified via precise DOM measurement (scrollHeight vs offsetHeight).
  • Tooltip: Active states use context-specific labels (e.g. "Expand post body" / "Collapse post body"). Disabled states include "Post fits within viewport without truncation" and (for expanded-but-small content) "Post body is small and doesn't need toggle".
  • Sticky Header Override: If clicked from the sticky header, this action also scrolls the viewport back to the original post header. Regular post header buttons do not trigger a scroll.

[PR-POSTBTN-06] Header Toggle Shortcut:

  • Clicking the post title or the background of a regular post header while it is already positioned at the top of the viewport (within 5px) triggers the [e] (Expansion) action.

[PR-POSTBTN-02] Load All Comments ([a]):

  • Fetches all comments for this post from the server using the postCommentsNew GraphQL view (request size must not be hard-capped below post.commentCount).
  • Merges fetched comments into the current state.
  • Re-renders the post group in place.
  • Button shows [...] while loading.
  • Disabled State: Disabled if all comments (per post.commentCount) are already loaded in state.
  • Tooltip: "Load all X comments for this post" (active); "All X comments already loaded" (disabled).

[PR-POSTBTN-03] Scroll to First Comment ([c]):

  • Scrolls the page so the first comment is at the top (accounting for sticky headers).
  • Disabled State: Disabled if post.commentCount === 0.
  • Tooltip: "Scroll to first comment" (active); "No comments to scroll to" (disabled).

[PR-POSTBTN-04] Scroll to Next Post ([n]):

  • Scrolls the page to the header of the next available post group in the rendered DOM order.
  • Disabled State: Disabled if this is the last post in the current feed.
  • Tooltip: "Scroll to next post" (active); "No more posts in current feed" (disabled).

[PR-POSTBTN-05] Collapse/Expand Post ([−]/[+]):

  • Toggles the visibility of the post body and its comments.
  • Sticky Header Override: If clicked from the sticky header, this action also scrolls the viewport back to the original post header. Regular post header buttons do not trigger a scroll. This prevents the user from being stranded in an empty scroll area after collapsing a long thread.

[PR-POSTBTN-07] Send to AI Studio ([g]):

  • Triggers the Google AI Studio integration for this post.
  • Identical to the g shortkey action.
  • Shift Modifier: Holding Shift while clicking or pressing G (Shift+g) includes all unread descendants of the target in the XML payload.
  • Tooltip: "Send thread to AI Studio (Shortkey: g, Shift-G includes descendants and fetches them if needed)".

Button Rendering:

  • Buttons appear in this order (left to right): [g] [m] [e] [a] [c] [n] ... [−]/[+]
  • Sticky header mirrors these buttons.
  • Disabled buttons use low opacity and cursor: not-allowed.

13. Comment Header Action Buttons

Action buttons are displayed in the comment header's .pr-comment-controls span, positioned to the left of the existing [−]/[+]/[^] buttons.

[PR-CMTBTN-01] Load Replies ([r]):

  • Fetches all descendants of the current comment (replaces old "Load All Descendants").
  • Uses commentReplies view recursively or full thread fetch logic.
  • Merges fetched comments into state.
  • Re-renders the post group.
  • Disable Rule: Always rendered, but disabled if the comment has 0 descendants (directChildrenCount == 0) OR if all its descendants are already loaded in state (recursive check, not just direct children). Tooltip explains why.

[PR-CMTBTN-02] Load Parents & Scroll ([t]):

  • Combined Action: "Trace to Root".
  • Primary Function: Scrolls to the top-level ancestor (Root) of the thread.
  • Data Loading: If the top-level ancestor (or any intermediate parent) is missing from the state, it recursively fetches the missing parents from the server before scrolling.
  • Viewport: Preserve the focal comment's viewport position across any re-render caused by [t]. Then scroll to Root only if Root is not fully visible.
  • Sticky-Aware Visibility: Visibility checks for [t] and [^] treat content under the visible sticky header as not fully visible.
  • Highlight: After loading/scrolling, flashes the root comment with .pr-highlight-parent.
  • Disable Rule: Always rendered, but disabled if the comment is already at the top level (no parent). Tooltip: "Already at top level".

[PR-CMTBTN-03] Send to AI Studio ([g]):

  • Triggers the Google AI Studio integration for this comment.
  • Identical to the g shortkey action.
  • Shift Modifier: Holding Shift while clicking or pressing G (Shift+g) includes all unread descendants of the target in the XML payload.
  • Tooltip: "Send thread to AI Studio (Shortkey: g, Shift-G includes descendants and fetches them if needed)".

Button Rendering:

  • Buttons appear in this order: [g] [m] [r] [t] ... [^] [−] [+]
  • Visibility Policy: Action buttons MUST never be hidden, only disabled (with cursor: not-allowed and reduced opacity). Disabled buttons MUST have a tooltip explaining why they are disabled.
  • Tooltips: All buttons MUST have descriptive tooltips (e.g., "[t]" -> "Load parents and scroll to root").
  • Author Controls: [↑] / [↓] must have tooltips explaining they affect author preferences.

[PR-GQL-01] GET_POST_COMMENTS: Fetch all comments for a post.

query GetPostComments($postId: String!, $limit: Int) {
  comments(
    selector: { postCommentsNew: { postId: $postId } },
    limit: $limit
  ) {
    results { ...standardCommentFields }
  }
}

[PR-GQL-02] GET_THREAD_COMMENTS: Fetch all comments in a thread by top-level comment ID.

query GetThreadComments($topLevelCommentId: String!, $limit: Int) {
  comments(
    selector: { repliesToCommentThreadIncludingRoot: { topLevelCommentId: $topLevelCommentId } },
    limit: $limit
  ) {
    results { ...standardCommentFields }
  }
}

[PR-GQL-03] GET_NEW_POSTS_FULL: Fetch posts in the enrichment window with body fields.

query GetNewPostsFull($limit: Int, $after: String, $before: String) {
  posts(
    selector: { new: { after: $after, before: $before } },
    limit: $limit
  ) {
    results {
      _id
      title
      slug
      pageUrl
      postedAt
      baseScore
      voteCount
      commentCount
      wordCount
      htmlBody
      contents { markdown }
      user { _id username displayName slug karma }
    }
  }
}

Both comment queries use the same fragment fields as GET_ALL_RECENT_COMMENTS (including topLevelCommentId as a new field to add to the fragment).


14. Quick Help Panel (Collapsible)

  • [PR-HELP-01] Collapsible panel at top explaining features.
  • [PR-HELP-02] First visit: Expanded by default.
  • [PR-HELP-03] Subsequent visits: Collapsed by default.
  • [PR-HELP-04] State saved via GM_setValue("helpCollapsed", boolean).
  • [PR-HELP-05] Multi-column layout: The help content uses a CSS multi-column grid (2-3 columns depending on viewport width) to be more compact. Each section (h4 + its list) stays together via break-inside: avoid.
  • [PR-HELP-06] Shortcuts documented: The help guide includes sections for Post and Comment Action Buttons, documenting both their icons/actions and their associated keyboard hotkeys.

15. Logged-In User Detection

  • [PR-USER-01] Detect current logged-in user via GraphQL currentUser query.
  • [PR-USER-02] Used to identify "replies to you" (comments where parent author = logged-in user).


16. Resizable View Width

  • [PR-LAYOUT-01] Draggable resize handles on both left and right sides of the content area.
  • [PR-LAYOUT-02] Width saved across sessions via GM_setValue("viewWidth", pixels).

17. Hover Previews

  • [PR-PREV-01] Hover Delay: 300ms.
  • [PR-PREV-02] Intentionality: Ignore scroll-induced hovers.
  • [PR-PREV-03] Scroll Blocking: Dismissed immediately on scroll.
  • [PR-PREV-04] Mouse leaving trigger area dismisses preview.
  • [PR-PREV-05] Click trigger = navigate same tab; Ctrl+click = new tab.

17b. Post Link Preview

  • [PR-PREV-06] Triggers on hover over post title links.

17c. Comment Link Preview

  • [PR-PREV-07] Detects comment URLs in body and shows full content preview.
  • [PR-PREV-08] Detects post URLs in comment body and shows preview.
  • [PR-PREV-09] Detects author URLs in comment body and shows preview.
  • [PR-PREV-10] Wiki Tag Preview via GraphQL: Wiki/tag previews (e.g. /tag/alignment) MUST load preview HTML via GraphQL (tags with tagBySlug) on the current forum endpoint (with EAF legacy adapter compatibility), not by scraping full page HTML.

18. Voting Buttons

  • [PR-VOTE-01] Karma voting is supported for posts and comments. Separate agreement-axis voting is supported for comments (LW-style/two-axis systems), not posts.
  • [PR-VOTE-02] Karma voting: [â–²] [â–¼] buttons.
  • [PR-VOTE-03] Agreement voting/display split by forum:
    • LW posts: agreement controls and agreement score display MUST NOT render.
    • LW comments: agreement axis [✓] [✗] buttons and agreement score are supported.
    • EAF posts/comments: agreement is supported via agree/disagree reaction chips (no separate agreement axis buttons/score).
    • Rationale: Native EA Forum presents Agree/Disagree as separate reactions rather than an LW-style agreement axis, so Power Reader mirrors that model on EAF.
  • [PR-VOTE-04] If logged in: Execute mutation, update UI optimistically.
  • [PR-VOTE-05] If not logged in: Open login page in new tab.
  • [PR-VOTE-06] On EA Forum host, agree and disagree reaction chips must remain visible for posts and comments even when their counts are 0.
  • [PR-VOTE-07] Rich Tooltips: Karma scores (and comment agreement scores where the agreement axis is rendered) show rich tooltips on hover, including:
    • Score label and value.
    • Description (e.g., "Total votes" or "Net agreement").
    • List of users who voted (for agreement score, including "Agree"/"Disagree" labels and quotes).

19. Reaction System

  • [PR-REACT-01] Scrapes reaction definitions from site webpack bundle.
  • [PR-REACT-02] Caches definitions for 7 days.
  • [PR-REACT-03] categorized reaction picker panel.
  • [PR-REACT-04] View Toggle: Grid View vs List View.
  • [PR-REACT-05] Search: Filter reactions by name/label.
  • [PR-REACT-06] Reaction Tooltips: Viewport-aware, rich tooltips for all reaction icons. Tooltips for reaction chips in post/comment headers MUST show:
    • Reaction label and icon.
    • Reaction description (if available).
    • List of users who reacted (usernames).
    • Attached quotes for inline reactions.
  • [PR-REACT-07] Inline Reactions: Select text to react with a quote.
  • [PR-REACT-08] Display: Reactions shown as chips in header with net scoring.
  • [PR-REACT-09] Single Reaction Control: Each post/comment metadata row MUST render exactly one reactions container and one add-reaction button; rerenders (including vote updates) MUST replace reaction contents in-place without duplicating controls.

  • [PR-AI-01] Shortkeys:
    • Press g to send to Google AI Studio.
    • Press m to send to Arena.ai Max.
  • [PR-AI-02] Automation: Opens the respective AI site, injects thread XML and prompt, and submits it in-page.
  • [PR-AI-10] Vote + Date Context in AI Payload: Thread XML MUST include postedAt timestamps and vote metadata for every serialized post/comment (lineage and descendants). Vote metadata MUST include karma score + vote count, and agreement data in both forum representations when available: LW-style agreement axis fields (agreement, agreementVoteCount, current user agreement vote) and EAF-style agree/disagree reaction counts + current-user agree/disagree state.
  • [PR-AI-11] Linkpost URL Context in AI Payload: For link posts only, AI thread XML MUST include the normalized linkpost target URL on the post entry via <link_url>. Comment entries (including descendants) MUST NOT include a separate linkpost URL tag. If the normalized linkpost URL resolves to the same canonical page URL as the post itself, the linkpost URL MUST be suppressed.
  • [PR-AI-03] Handoff-Only Output: The reader MUST NOT render provider responses in-page. Output is viewed directly in the opened AI Studio/Arena tab.
  • [PR-AI-04] Descendant Mode: Shift-triggered AI actions (Shift+G, Shift+M, and Shift-click variants) include descendants in the payload, with the large-descendant confirmation policy applied.
  • [PR-AI-05] Depth: Recursively fetches parent comments up to the root (no fixed depth cap), with cycle-safe traversal to avoid infinite loops on malformed parent links.
  • [PR-AI-06] Grounding: Automatically disables Google Search grounding in AI Studio.
  • [PR-AI-07] URL Context: Automatically enables the "URL context" tool in AI Studio.
  • [PR-AI-08] Arena Automation: For Arena.ai, the script MUST inject into the "Ask anything..." textarea and click the send button. If 2 seconds after submission the URL path still starts with /max, it MUST retry submission once. It MUST handle Cloudflare challenges by allowing the user to manually verify if blocked.
  • [PR-AI-09] Native Forum Handoff: On native LW/EAF pages (non-/reader), g/G/m/M hotkeys MUST use the same payload + tab handoff pipeline as reader mode, resolving hovered comment/post targets from forum DOM/permalink context.

21. Author Preferences

  • [PR-AUTH-05] Preference Controls: ↓ / ↑ arrows in all comment and post headers.
  • [PR-AUTH-06] Persistence: Preferences are saved locally via GM_setValue.
  • [PR-AUTH-07] Global Update: Toggling a preference for an author immediately updates all visible comments and posts by that author across the entire interface.
  • [PR-AUTH-08] Visual Feedback: Preferred authors are highlighted (pink meta header); disliked authors are auto-hidden (collapsed by default).

22. Keyboard Shortcuts (Hotkeys)

[PR-HK-01] Hover-Triggered Actions: Pressing a shortcut key while hovering over a post or comment triggers the corresponding action button for that specific item.

  • [PR-HK-02] Post Hotkeys:
    • e: Toggle post body (Expand/Truncate/Load)
    • a: Load all comments
    • c: Scroll to comments
    • n: Scroll to next post
    • g: Send to AI Studio
    • G (Shift+G): Send to AI Studio with descendants
    • m: Send to Arena.ai Max
    • M (Shift+M): Send to Arena.ai Max with descendants
    • -: Collapse post and comments
    • + / =: Expand post and comments
  • [PR-HK-03] Comment Hotkeys:
    • r: Load replies
    • t: Trace to root (load parents and scroll)
    • g: Send to AI Studio
    • G (Shift+G): Send to AI Studio with descendants
    • m: Send to Arena.ai Max
    • M (Shift+M): Send to Arena.ai Max with descendants
    • p / ^: Find parent (scroll)
    • -: Collapse comment
    • + / =: Expand comment
  • [PR-HK-04] Input Suppression: Hotkeys MUST be ignored when the user is typing in an <input>, <textarea>, or any contenteditable element to prevent accidental triggers.
  • [PR-HK-05] Alias Support: The = key is supported as an alias for + (Expand) to allow one-handed use without requiring the Shift key.
  • [PR-HK-06] Feedback: Successful hotkey triggers are logged to the console (Info level) and provide immediate visual feedback via the triggered action (e.g., scroll, collapse, or loading state).
  • [PR-HK-07] Post Action Fallback: If a post-level hotkey (e.g., n, a, c, e) is pressed while hovering over a comment, the system MUST automatically identify the parent post and trigger the corresponding action on that post. This ensures seamless navigation without requiring the user to precisely hover the post header.
  • [PR-HK-08] Native Forum AI Hotkeys: On native LW/EAF pages in forum-injection mode, g/G/m/M MUST trigger AI send actions for hovered comment/post targets, with input/modifier suppression, selection-aware suppression, TOC/navigation exclusion zones, and stale-operation handoff suppression on route re-init.

23. GraphQL Codegen & Developer Workflow

  • [PR-DEV-01] Automated Codegen: The project uses tooling/maybe-codegen.js to automatically run graphql-codegen during dev and build tasks, but ONLY if queries.ts or the schema has changed. This maintains type safety while minimizing build overhead.

24. Tree-Karma Sorting

  • [PR-SORT-01] Tree-Karma Definition: For any item (Post or Comment), its Tree-Karma is the maximum baseScore among all unread items (including the root itself) in the tree rooted at that item.
    • An item is considered "unread" if its ID is not present in the local ReadTracker dictionary.
    • If the tree (including the root) contains no unread items, something has gone wrong (as the feed should only contain unread content); in this case, the Tree-Karma is -Infinity and a warning must be logged to the console.
  • [PR-SORT-02] Top-Level Sorting: Posts in the main feed are sorted by their Tree-Karma descending.
  • [PR-SORT-03] Hierarchical Comment Sorting: At every level of the comment tree (top-level comments under a post, and replies within a comment group), items are sorted by their Tree-Karma descending.
  • [PR-SORT-04] Stability Policy: Sorting is calculated during the rendering phase. To avoid UI instability, items MUST NOT be reshuffled dynamically as the user marks them "read" during a scroll session. The updated sort order will be reflected on the next full render or data load.

25. Forum Header Injection

[PR-INJECT-01] The userscript injects a link to the Power Reader into the main site header on all forum pages (matching *).

  • Target Container: .Header-rightHeaderItems
  • Link Text: "POWER Reader" (with "POWER" styled as a badge: dark background, bold white text).
  • Placement: Positioned after the search bar (if present) or at the start of the right-side items.
  • Hydration Safety: Injection is delayed by 2 seconds after page load or SPA navigation to avoid React hydration mismatches (Error #418).
  • Mutation Observer: A MutationObserver ensures the link persists during client-side navigation within the forum.

26. User Archive Mode

The Power Reader supports a dedicated "User Archive" mode for browsing a user's entire history (posts and comments) in a unified, searchable feed.

  • [PR-UARCH-01] Route Activation: Archive mode activates only on /archive?username=[username].
  • [PR-UARCH-02] Missing Username Fallback: If /archive is present but username is missing, the route falls back to normal /reader mode (no archive init).
  • [PR-UARCH-03] Cache-First Boot: On init, the archive MUST load IndexedDB data first and render cached items immediately when available.
  • [PR-UARCH-48] Empty-Cache First Render Timing: If archive init starts with an empty local cache, the first archive render MUST be deferred until the initial sync attempt returns, rather than firing from network-idle timing mid-fetch.
  • [PR-UARCH-04] Background Sync: After cache load, archive mode performs incremental sync for posts/comments newer than the stored watermark, using edit-aware server time fields (comments.timeField = "lastEditedAt", posts.timeField = "modifiedAt").
  • [PR-UARCH-15] Adaptive Cursor Pagination: The background sync loader uses cursor-based pagination (after) to bypass API offset limits (typically 2000 items). It dynamically adjusts pagination limit (50-1000 items) to target ~2.5s per batch for reliability on unstable networks. Cursor advancement/watermarks are based on the active sync time field (with postedAt fallback when needed), while canonical display order remains postedAt descending.
  • [PR-UARCH-05] Stable Sync Watermark: The persisted lastSyncDate watermark MUST be a stable value captured at sync start (not sync end) to avoid skipping in-flight items created during the sync window.
  • [PR-UARCH-06] Non-Destructive Sync Failure: If background sync fails after cached items are shown, the UI MUST keep showing cached data and report sync failure in the status line. Failure status MUST be highlighted in red/bold to differentiate from normal status.
  • [PR-UARCH-16] Status Line Indicators: While syncing, the status line MUST show an animated indicator (...). Success/Progress messages use neutral colors; Errors use status-error styling.
  • [PR-UARCH-14] Manual Resync Recovery: A "Resync" button allows users to bypass the lastSyncDate watermark and force a full history download from the server to recover from corrupted local states.
  • [PR-UARCH-07] Merge Semantics: Archive sync upserts by _id (same-ID items replace stale cache rows so edits propagate), remains duplicate-free, and keeps canonical in-memory order sorted by postedAt descending.
  • [PR-UARCH-17] Payload Optimization: To minimize bandwidth and prevent GraphQL timeouts, archive comment fetches MUST use a "Lite" post fragment (metadata only) rather than full post bodies for parent references.
  • [PR-UARCH-08] Structured Search Behavior: Archive search uses a structured query grammar (author:, replyto:, date:, score:, type:, scope:) with plain terms and quoted phrases. Regex is supported only through explicit regex literals (/pattern/flags). Invalid regex literals MUST produce a non-fatal warning and be excluded from execution.
  • [PR-UARCH-39] Search Result Count Status: The archive top status line (#archive-status) MUST include the current search result count (N search result(s)) alongside sync/status messaging.
  • [PR-UARCH-40] Strict Date Filter Validation: Structured date: filters MUST reject impossible calendar dates (for example, 2025-02-31) instead of normalizing overflowed dates.
  • [PR-UARCH-09] Sort Modes: Archive supports relevance (search ranking), date (newest), date-asc (oldest), score, score-asc, and replyTo.
  • [PR-UARCH-10] View Modes: Archive supports card, index, and thread views.
  • [PR-UARCH-11] Thread Context Loading: In thread view, missing parent comments for visible items are fetched and injected into context maps before final render.
  • [PR-UARCH-25] Context Isolation: Context comments (fetched for thread view ancestry) MUST remain excluded from the canonical ArchiveState.items collection. In card view they may render only as immediate-parent context for canonical comments; index rows remain canonical-only.
  • [PR-UARCH-22] Context Persistence: Context comments fetched during a thread view session MUST be preserved across rerenders (e.g., when switching sort modes or loading more items) within that session.
  • [PR-UARCH-34] Persistent Context Cache: Non-authored context items (ancestor comments and root posts used for thread reconstruction) MUST be persisted in IndexedDB under a dedicated contextual cache and scoped by target archive username.
  • [PR-UARCH-35] Context Resolution Waterfall: Thread context loading MUST resolve parents in this order: (1) authored/canonical in-memory state, (2) contextual IndexedDB cache, (3) network fetch for remaining IDs only.
  • [PR-UARCH-36] Canonical Post Integrity: Context-derived lite post payloads MUST NOT overwrite canonical authored post objects (which may contain full body fields). Canonical post updates require non-downgrading merge semantics.
  • [PR-UARCH-37] Partial-Batch Pagination Safety: Archive cursor pagination MUST base continuation/cursor progression on raw server batch envelopes, not on client-filtered valid-item counts, to avoid premature termination when some rows are invalid.
  • [PR-UARCH-38] Missing Parent Object Tolerance: A comment may have parentCommentId without an inline parentComment object. Thread context loading MUST still fetch that parent by ID and continue reconstruction.
  • [PR-UARCH-12] Bounded Rendering Without Manual Pagination: Archive renders up to the configured render cap for the active result set and MUST NOT expose user-clickable "Load More" controls.
  • [PR-UARCH-24] Sort Stability Across Rerenders: The active sort mode (including thread-specific group sorting) MUST persist and correctly apply when archive rerenders are triggered by UI state changes.
  • [PR-UARCH-26] Post-Rerender Initialization: UI hooks like link previews and post action buttons MUST be re-initialized after archive rerenders.
  • [PR-UARCH-18] Large Dataset Safety: When an archive contains > 10,000 items, the UI MUST show a confirmation dialog before rendering to protect browser performance. Once the user selects a render count (or "Render All"), this preference MUST persist across sorting, filtering, and view mode changes for the duration of the session.
  • [PR-UARCH-13] Author Preview Integration: Author hover previews include a direct archive link (📂 Archive) targeting /archive?username=[slug-or-username].
  • [PR-UARCH-19] ReaderState Identity Stability: After archive rerenders (triggered by sort/filter changes or sync updates), event handlers bound to the ReaderState reference MUST continue to function correctly. The ReaderState object identity MUST be preserved across rerenders (mutated in place rather than replaced) to prevent stale references in event listener closures.
  • [PR-UARCH-23] Authentication Context: The archive ReaderState MUST be correctly populated with the currentUserId and currentUsername of the logged-in user to enable authenticated actions like voting and reactions within the archive view.
  • [PR-UARCH-20] Thread Group Date Sorting: In thread view, post groups are sorted based on the timestamp of the newest item within that group (post or comment).
  • [PR-UARCH-21] Thread Group Karma Sorting: In thread view, post groups are sorted based on the highest karma score among all items within that group.
  • [PR-UARCH-27] Context Type Enum: Comments use a unified contextType field ('missing' | 'fetched' | 'stub' | undefined) instead of separate isPlaceholder and isContext boolean flags to represent different context states.
  • [PR-UARCH-28] Thread View Modes: Archive supports two thread view modes: thread-full (fetches full parent context from server) and thread-placeholder (creates stub context from parentComment references without network requests).
  • [PR-UARCH-29] Card View Parent Context: In card view, comments with a parent comment render with that immediate parent as the outer node and the current comment nested under .pr-replies (using fetched parent context when available, otherwise a metadata-only stub placeholder at 80% font size). Top-level comments render under their parent post header in the same nested .pr-replies structure for consistency with thread/post views.
  • [PR-UARCH-30] Index View Expand: In index view, clicking any item expands it in-place to card view rendering with a "Collapse" button to return to the index row.
  • [PR-UARCH-31] Placeholder Context Rendering: Comments with contextType: 'stub' render as header-only placeholders without vote buttons, using .pr-context-placeholder CSS class.
  • [PR-UARCH-32] Context Persistence Across Modes: Context comments (both 'fetched' and 'stub') are preserved in ReaderState when switching between thread, card, and index views within the same session.
  • [PR-UARCH-33] Thread Mode IsThread Helper: A helper function isThreadMode() identifies both thread-full and thread-placeholder as thread variants without requiring multiple equality checks.
  • [PR-UARCH-41] Search Worker Required: Archive search MUST run in a Web Worker. Runtime/fallback search paths are not supported. If worker initialization fails, archive initialization fails with an explicit error.
  • [PR-UARCH-42] Engine Initialization Warning Resilience: Initialization handles partial index building gracefully. If Worker throws warnings during initial sync or cache loading, it resolves correctly instead of throwing fatal exceptions.
  • [PR-UARCH-43] Segmented Scope Toggle: The UI utilizes a custom segmented control (.pr-segmented-control) for the authored vs. all scopes. This logic securely bounds state switches and updates ArchiveState uniformly relative to URLs.
  • [PR-UARCH-44] Custom View Mode Selection: The UI uses an icon-tab structure (.pr-view-tabs) matching Power Reader norms instead of standard dropdowns. Values trigger correct event emission and URL synchronization.
  • [PR-UARCH-45] Debug Explain Output: Utilizing the query parameter ?debug=1 turns on relevance transparency, outputting diagnostic math alongside results (tokenHits, phraseHits, internal scoring bounds).
  • [PR-UARCH-46] Interactive Facet Filtering: Clickable dynamic .pr-facet-chip elements populate based on current result aggregations. Activating a facet updates the native search bar text string and automatically dispatches a new search via event triggering.
  • [PR-UARCH-47] Eager Parent Preloading: To minimize network round-trips and improve responsiveness in thread views, the archive sync loader eagerly requests the htmlBody and contents of immediate parent comments during the initial authored history download. These parent items are extracted and automatically populated into the persistent contextual cache.
  • [PR-UARCH-49] Sticky Archive Controls: The archive toolbar and facet chip row are rendered in a sticky header with scroll-state styling so search/sort/view controls remain pinned during feed scrolling.
  • [PR-UARCH-50] Help Popover Control: Search syntax help is exposed via an icon-triggered popover (#archive-search-help-btn / #archive-search-help-popover) that supports outside close via pointerdown, Escape, and example-click close behavior.
  • [PR-UARCH-51] Idle Search Status Collapse: The archive search status row (#archive-search-status) remains hidden when there are no messages and only appears for active diagnostics/info chips.
  • [PR-UARCH-52] Compact Toolbar Density: Archive top controls use compact icon actions, horizontal view tabs, and grouped spacing to reduce vertical footprint without removing existing sort/scope/view functionality.
  • [PR-UARCH-53] No-Query Baseline Facets: When the query is empty, archive facets render immediately from cached per-scope baseline aggregates keyed by sync watermark (and all-scope context count). If a fresh exact baseline is unavailable, sampled counts are shown first and refined asynchronously when feasible.
  • Detailed Specification: See ARCH_USER_ARCHIVE.md for implementation architecture notes.

27. User Archive Link Injection

  • [PR-INJECT-02] Archive Link Injection: On user profile routes (/users/slug and /users/id/slug), a "User Archive" link is injected into the site header next to the "POWER Reader" link. The link targets /archive?username=[slug].
  • [PR-INJECT-03] Shared Container: Both the Reader link and Archive link (when present) are wrapped in a shared container (#pr-header-links-container) for consistent styling and positioning.
  • [PR-INJECT-04] URL-Based Visibility: The Archive link is only visible when the current URL path starts with /users/. No SPA navigation handling is required since LW is not a SPA.

Technical Requirements

Userscript Metadata

[PR-TECH-01] Required Metadata Block:

// ==UserScript==
// @name         LW Power Reader
// @namespace    http://weidai.com/
// @match        https://www.lesswrong.com/*
// @match        https://forum.effectivealtruism.org/*
// @match        https://aistudio.google.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_log
// @run-at       document-start
// @connect      lesswrong.com
// @connect      forum.effectivealtruism.org
// ==/UserScript==