Skip to content

feat: collaborative Wikipedia browsing — link glow, cursor following, and tooling#80

Open
spencerc99 wants to merge 60 commits intomainfrom
feat/wiki-link-glow
Open

feat: collaborative Wikipedia browsing — link glow, cursor following, and tooling#80
spencerc99 wants to merge 60 commits intomainfrom
feat/wiki-link-glow

Conversation

@spencerc99
Copy link
Owner

Summary

Adds collaborative browsing features to the browser extension for Wikipedia, plus developer tooling for demos and load testing.

Link Glow (Patina)

  • Nebula-like glow on Wikipedia links based on shared click-through data via playhtml.createPageData()
  • Dual-path rendering: pseudo-element blur for single-line links, inline backgrounds with box-decoration-break: clone for multi-line
  • S-curve opacity scaling (barely visible at 1 click, strong at high counts)
  • Shared rendering code between extension and preview page (@extension Vite alias)
  • Decay mechanism to prevent unbounded data growth

Cursor Following

  • Press F near another cursor to follow them
  • Scroll tethering (pauses on manual scroll, re-engages when followed cursor moves)
  • Auto-navigate countdown (3-2-1) when followed person clicks an article link
  • Navigation delay only when someone is following (solo clicks are instant)
  • "Being followed" chrome (top bar showing follower color dots)
  • Off-screen cursor indicators at viewport edge
  • Proximity hint tracks cursor position via presence API

Presence & Lobby

  • "N here" pill in top-right showing page visitors
  • "M elsewhere" count with portal jump button via createPresenceRoom("lobby")
  • Domain-wide lobby for cross-page awareness

Architecture

  • extension/src/custom-sites/ dispatcher pattern for domain-specific features
  • wikipedia.ts module handles all Wikipedia-specific initialization
  • Migrated from hidden element hack to playhtml.createPageData() and playhtml.presence APIs
  • Typed WikiPresenceView for presence data (no as any casts)

Developer Tooling

  • tools/playwright/ — Browser choreography for scripted multi-actor demos with extension loading, setup flow completion, video recording, and organic cursor movement
  • tools/load-test/ — Moved from root, added lobby-presence scenario (tested 500 users successfully)

Preview Page

  • Preview imports shared renderer from extension via @extension alias
  • Removed duplicate rendering code (single source of truth)

Test plan

  • Manual: two browser windows on same Wikipedia article, verify cursors visible
  • Manual: proximity hint appears when cursors are within 250px
  • Manual: press F to follow, verify vignette + status bar + scroll tether
  • Manual: followed person clicks article link, verify countdown toast + auto-navigate
  • Manual: verify link glow appears after clicking links (check with vivid vs muted colors)
  • Manual: verify presence pill shows count and jump button works
  • Playwright: bun tools/playwright/src/runner.ts --scene wiki-follow-demo
  • Load test: bun tools/load-test/src/runner.ts --scenario lobby-presence --users 500

…ty scaling

- Logarithmic + absolute dampener intensity scaling (ABSOLUTE_RATE=50)
  so single clicks barely show and glow grows naturally with traffic
- Single-line links: pseudo-element blur via injected stylesheet
- Multi-line links: inline backgrounds with box-decoration-break + drop-shadow
- Inline opacity reduced to 40% to compensate for no blur
- Decay mechanism (subtract 5 every 100 total clicks) prevents unbounded growth
- Preview page: early-visit test cases, hover tooltips, click-to-copy data
- Remove debug console.log statements, fix GlowStyle import
- LinkGlowManager: replace hidden <div can-play> hack with
  playhtml.createPageData() for persistent shared link click data
- FollowManager: replace broken window.cursors callbacks with
  playhtml.presence API for reading/writing ephemeral state
- Remove manual proximity detection, use cursor client's built-in
  onProximityEntered/onProximityLeft callbacks instead
- Broadcast navigatingTo/following via presence.setMyPresence()
- Create extension/src/custom-sites/ with domain dispatcher pattern
- wikipedia.ts: link glow init + delayed navigatingTo broadcast
- content.ts: delegate to initCustomSite() instead of inline Wikipedia code
- Move isWikiArticleUrl from FollowManager to wikipedia module
- Track last seen navigatingTo so follower sees nav toast even if
  target disconnects before the poll catches it
- Clear stale navigatingTo when restoring follow from sessionStorage
- Poll at 150ms instead of 200ms for better responsiveness
- OffscreenIndicator: circular pip at viewport edge showing direction
  to cursors above/below the viewport, colored by cursor identity
- FollowManager: detect when others are following you, show subtle
  top bar with follower color dots and count
- Scroll tether tick updates off-screen indicator for followed cursor
- Follower watcher updates indicators for followers' cursors
Follow hint, scroll tether, navigation following, and off-screen
indicators are now Wikipedia-only. Proximity callbacks are wired
via cursorClient.configure() in wikipedia.ts instead of at init.
- PresenceCountPill now shows "N here · M elsewhere [portal]"
- Portal button jumps to a random page where someone else is browsing
- Domain-wide lobby created via createPresenceRoom("lobby")
- Each user broadcasts their current page URL/title into the lobby
- Depends on playhtml.createPresenceRoom (pending core PR)
- Fix nav watch interval leak: track separately, clear on unfollow
- Only delay navigation when user has followers (solo clicks are instant)
- Guard lobby creation with runtime check for missing createPresenceRoom
- Track leftToast timeout, clear on unfollow to prevent double-fire
- Remove dead navToastTimeout field
… test

- Hint now positioned near the other cursor and updates as they move
- Multiple nearby cursors: only shows hint for the closest one
- nearbyCursors map tracks all nearby cursors with distances
- lobby-presence load test scenario: 10k users in a domain-wide lobby
  each broadcasting a random Wikipedia page URL via awareness
- Extract applyGlowToLink() and buildGlowCssRules() as standalone
  exported functions in link-glow-renderer.ts
- LinkGlowManager delegates to shared functions instead of inline logic
- Preview SmearLink uses the same shared renderer via cross-import
- Preview linkIntensity() delegates to shared computeIntensity()
- Single source of truth for glow visual logic
…move debug logs

- Apply OPACITY_MUL=0.6 to pseudo-element blur path (matching preview tuning)
- Inline path now uses 0.6 * 0.4 = 0.24 effective opacity
- Add z-index: 1 to <a> so text renders above pseudo-element glows
- Save/restore position and z-index in original styles
- Remove all debug console.log statements
- Extract applyInlineGlow, applySingleLineGlow, buildPseudoElementCSS
  as standalone exported functions in link-glow-renderer.ts
- LinkGlowManager delegates to shared functions instead of inline logic
- Preview's SmearLink imports from extension renderer via cross-import
- Remove duplicate averageHex, pairColors, smearNebulaLayers,
  buildGlowShadows, ltHexToRgba from preview
- Remove opacity/saturation tuning sliders (now baked into renderer)
- Single source of truth for all glow rendering math and DOM application
…st click

- Add @extension alias in vite.config.site.mts for website to import
  from extension/src/
- Lower base opacity constants so count=1/pageMax=1 gives ~0.03 alpha
  instead of 0.09 (vivid player colors were too prominent at low counts)
data.t is the CSS selector of the input field, not the typed text.
domain field is missing from events, must be parsed from meta.url.
Also trim unused fonts from conversations.html.
Detects messages where >70% of characters are the same char
(e.g. "sssssss", "rrrrrr") and excludes them.
Add white-space: pre-wrap so \n characters from Enter keys
render as actual line breaks in message bubbles.
The old approach diffed textContent before/after each input event,
assuming new text was always appended at the end. This caused:

1. Repeated character spam on contenteditable (Gmail, Instagram DMs)
   because editor DOM reflows triggered phantom text changes via
   textContent reading internal/duplicate DOM nodes.

2. Corrupted text when typing in the middle of existing text, since
   textAfter.slice(textBefore.length) captures the wrong characters.

Now uses InputEvent.inputType and InputEvent.data which the browser
provides directly, eliminating the need for text diffing entirely.
Also switches from textContent to innerText for contenteditable
elements, which respects rendered content rather than raw DOM nodes.
Toggle with double-tap 'd'. Shows:
- Start time datetime picker with clear button
- Message count (filtered / total)
- Restart animation button
- Domain histogram with favicons and bar chart

Also fixes default start time to show all data instead of
filtering to a hardcoded date.
Config panel now renders as a sticky sidebar on the left instead of
inline in the conversation. Subtitle always shows "starting from"
with either the filtered start time or the earliest message date.
After all messages have been shown, pauses 3 seconds then
resets and replays from the beginning. Scrolls back to top
on each loop iteration.
Client fetches keyboard events in pages of 5000 using the 'to'
date param. When the animation exhausts the current batch, it
requests the next older page. When all data is exhausted, loops
back to the beginning after a 3-second pause.

Animation state is preserved across page fetches so it resumes
from where it left off rather than restarting.
Replaces datetime-local input with a select dropdown showing
all available dates with message counts (e.g. "2026-01-25 (42)").
"all dates" option shows total count. Selecting a date sets the
start time filter.
Click any domain in the histogram to exclude/include it.
Excluded domains are dimmed and their messages are filtered out.
Setting persists in localStorage across sessions.
- Day/time sort mode toggle: "by day" (chronological) vs "by time
  of day" (merges messages across days, sorted by time-of-day)
- Max consecutive per domain slider (0-20, default 4): caps runs
  of same-domain messages for more varied conversation flow
- Domain search bar: filter the domain list to find/toggle any domain
- Domain list now shows all domains (scrollable) instead of top 15
- All settings persist in localStorage
Time mode no longer shows date headers since messages are merged
across days. A time slider (00:00-23:30, 30min steps) appears in
the config panel when time mode is active, letting you set what
time of day to start the conversation from. Persists in localStorage.
Speed slider in config panel scales all animation timings:
typing indicator duration, character type-in rate, and
inter-message pauses. Uses a ref so speed changes take
effect immediately without restarting the animation.
Persists in localStorage.
Use scrollIntoView on the last stream element so the whole page
scrolls (not just the stream div). Also trigger scroll on typing
text changes so it keeps up during character-by-character animation.
Speed slider max reduced from 20x to 10x.
Auto-scroll locks on when at the bottom of the page, unlocks when
you scroll up. A floating "follow" button appears in the bottom-right
when unlocked, clicking it re-locks and scrolls to the latest message.
Unlocks only when user scrolls up by >10px (not on every scroll).
Re-locks when user scrolls to near bottom. Programmatic scrolls
from scrollIntoView are ignored via a ref flag with 500ms cooldown.
…onUpdate

The onUpdate callback fires via queueMicrotask which may be too late
if the page is navigating away. Read data synchronously after setData
and call renderGlows immediately.
… bounds

The proximity callback provides document (page) coordinates, but the
hint uses position: fixed which needs viewport coordinates. Subtract
scroll position and clamp to viewport edges.
- Move load-test/ to tools/load-test/
- Add tools/playwright/ for browser choreography and demo recording
  - Scene definition format with defineScene()
  - Runner with extension loading, setup flow completion, video recording
  - smoothMove with eased cubic + organic wobble
  - wiki-follow-demo scene: two cursors meet, follow, navigate together
- Add tools/playwright/videos and .superpowers to gitignore
- Remove 5 debug console.log statements from production code
- Add ABOUTME header to content.ts
- Fix hint tracking to use presence API instead of fragile DOM query
  (correctly matches target by publicKey, works with 3+ cursors)
- Rename onProximityLeft param from connectionId to publicKey for clarity
- Define WikiPresenceView type, replace all as-any casts on presence data
- Add recordActor to SceneConfig interface
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant