Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# The checks this repo has, run by something other than a person remembering.
#
# It cannot replace the manual step in CONTRIBUTING.md — loading the unpacked extension in
# Chrome and watching it work on the live page — because a runner has no logged-in Forge.

name: CI

on:
# Limited to main on purpose: unrestricted, every push to a PR branch would run twice.
push:
branches: [main]
pull_request:

# Least privilege: these checks read the tree and nothing else.
permissions:
contents: read

# A new push to the same branch makes the previous run's answer irrelevant.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v7

# Pinned rather than inheriting the runner's default, which moves without anyone
# deciding it should. Matches engines.node in package.json.
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
cache-dependency-path: package-lock.json

- run: npm ci

# First because it is the cheapest thing that can fail.
- name: Lint
run: npm run lint

- name: Tests
run: npm test

- name: ADR log is current
run: python3 scripts/generate-adr-log.py --check
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.DS_Store

# Saved copies of real pages (logged-in Forge, W&B, …) for local selector work. Never commit
# them: this repo is public. Committed fixtures go in test/fixtures/ and must be synthetic.
test/fixtures/private/
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# engines.node is a floor, not advice: fail at install naming `engines`, not later at lint.
engine-strict=true
28 changes: 20 additions & 8 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Presenter

Chrome extension for live product demos: a pointer and fading highlight boxes drawn on the page.
Chrome extension for live demos and UX-proposal clips: pointer, highlights, toggleable enhancements.

Written in the W&B era to present W&B pages, so it injects only on the domains listed in `manifest.json`. It is jdoc's own tool, not a company product — it changes how a page *looks to the audience*, never what the page does.

## Language
## Language — presenting

**Pointer**:
The small dot that follows the cursor so the audience can track it — grey when idle, red when **armed**.
Expand All @@ -18,21 +18,33 @@ _Avoid_: active, drawing mode, enabled.
A rectangle drawn by dragging while **armed**, which fades out on its own a few seconds later. One drag makes one highlight; highlights are never saved or edited.
_Avoid_: annotation (implies it persists), box, selection.

**Demo zoom**:
The fixed zoom the extension applies on load so a shared screen is readable — only when the window holds exactly one tab, so a normal browsing window is left alone.
_Avoid_: scaling, magnify.
## Language — UX proposals

**Enhancement**:
One proposed change to a product page — a relabel, a colour, a reworded message — defined as a file in `enhancements/` and switched on or off from the popup. An enhancement targets specific pages and, when off, leaves no trace on them.
_Avoid_: mod, patch, fix (it proposes; it fixes nothing), tweak, userstyle.

**As shipped / Proposed**:
The two views of a page: **as shipped** is the real product with every enhancement removed; **proposed** has the switched-on enhancements applied. The before/after shortcut flips between them; one recording shows both.
_Avoid_: before/after as nouns for the views (they describe the clip, not the page), original, modified, live.

**Enhanced badge**:
The small "Enhanced" tag in the top-right corner, present exactly while the **proposed** view differs from **as shipped** — so a viewer of a clip always knows which one they are seeing.
_Avoid_: watermark, label, indicator.

## Language — both

**Target site**:
A domain the extension injects on, as listed in `manifest.json`. Anything else is untouched.
_Avoid_: allowed site, whitelist.

## Flagged ambiguities

**Forge is not yet a target site.** `wandb.ai` now redirects some users to `forge.coreweave.com`, which the manifest does not list — so the extension can silently disappear mid-demo after the redirect.
**"Before/after"** names the recording, not a view. A clip goes from **as shipped** to **proposed**; say those when you mean the state of the page.

## Example dialogue

> **Dev:** If I hold Shift and click a button on the page, does the button fire?
> **jdoc:** No — while you're **armed** the **pointer** swallows mouse events, so you get a **highlight**, not a click. Let go of Shift and the page behaves normally again.
> **Dev:** And the zoom jumped to 130% in my main browser window.
> **jdoc:** It shouldn't have — **demo zoom** only applies when the window has a single tab. Open the demo in its own window and keep browsing elsewhere.
> **Dev:** I switched on an **enhancement**, but the page looks the same.
> **jdoc:** Check the **Enhanced badge**. No badge means you're on **as shipped** — press the shortcut to flip to **proposed**. If the badge is there and nothing changed, the site's markup moved and the enhancement's selector no longer matches.
123 changes: 123 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Contributing to presenter

This guide covers what is **particular to presenter**: the rules a public repo imposes, the disciplines an unpacked Chrome extension with a pinned ID imposes, and where its bugs cluster. For what the extension does and how to use it, see [`README.md`](README.md); for its vocabulary, [`CONTEXT.md`](CONTEXT.md).

Ordinary git and GitHub practice — opening an issue, naming a branch, writing a Conventional Commit, opening a PR — is deliberately not restated here.

## Public repository: secrets, PII and sensitive information

**This repo is public (MIT). Everything committed, and everything written in its issues, PRs and commit messages, is published.** Presenter is used on logged-in work products, so the material it is built and tested against is often confidential. These rules are strict on purpose; when in doubt, leave it out.

**Never commit or post:**

- **Credentials of any kind** — API keys, tokens, cookies, session IDs, `.env` files, private keys. The `key` in `manifest.json` is a *public* key and is the one exception ([ADR-0002](docs/adr/0002-pin-the-extension-id.md)); its private half was never kept, and must never be generated and committed.
- **Saved copies of real pages.** A page saved from a logged-in site carries names, emails, org and project slugs, resource IDs and internal URLs in its markup. Keep real saves in the gitignored `test/fixtures/private/`, and write committed fixtures in `test/fixtures/` **by hand**: the smallest synthetic markup that reproduces the structure, with placeholder text (`acme`, `someone@example.com`, `00000000-0000-0000-0000-000000000000`). `test/fixtures-hygiene.test.js` fails on a real-looking email or UUID, but it is a backstop, not a licence — it cannot recognise a name or an org slug.
- **Personal data** — names, emails, avatars or IDs of anyone but the author, and anything identifying a customer or account.
- **Internal information** — internal Slack channels and threads, unreleased features or roadmap, internal URLs or hostnames, rollout plans, who said what, and screenshots of internal tools.
- **Clips and screenshots.** Loom recordings and images of enhanced pages stay out of the repo; link to them from wherever they are shared internally, never from here.

**Write enhancements about the UX, not about the context.** An enhancement's `title` and `problem` describe what a user sees and why it is confusing ("A stopped sandbox is shown as a green 'completed', which reads as healthy"), never the internal discussion that prompted it, who raised it, or which account hit it. The same goes for comments, tests, commit messages and PR descriptions.

**If something slips through:** removing it in a new commit does not unpublish it — it stays in history and in any fork. Tell the author straight away; rotating a credential comes first, rewriting history second.

See [`docs/security.md`](docs/security.md) for the extension's security model and a reviewer checklist.

## Workflow

Issue → worktree → PR → squash-merge → **reload the extension from the primary checkout**.

- **Work happens in a git worktree, never on the primary checkout.** `~/repos/presenter` stays a clean anchor on `main`, because it is the folder Chrome loads day to day. Worktrees are `presenter-<issue>-<slug>`; the branch is `<type>/<issue>-<slug>`.
- **Run `npm install` once in a new worktree.** It installs the dev tooling only — the extension itself has no build and no runtime dependencies — and writes a `.metadata_never_index` marker so Spotlight stays out of `node_modules` (see *Finishing*).
- **Node 24 or newer.** `.npmrc` sets `engine-strict=true`, so an older node fails at install, naming `engines`, rather than later and obscurely at lint.

### Bump the version as the worktree's first commit

```bash
./scripts/bump.sh # patch (the floor)
./scripts/bump.sh minor # a new feature
./scripts/bump.sh major # a breaking change
./scripts/bump.sh --dry-run # print the number it would take, change nothing
```

The script counts from the **highest version across every active worktree** — two branches from the same `main` must never claim the same number — writes `manifest.json`, and commits `chore(release): bump to <version>`. It refuses to run on `main`.

**Why first:** every build shares one pinned extension ID ([ADR-0002](docs/adr/0002-pin-the-extension-id.md)), so only one build is live at a time, and the version at `chrome://extensions` (also shown in the popup and logged to the page console on load) is the only marker of which. **`manifest.json` is the single source of the version** — there is no build to inject it, and `package.json` deliberately has none; `test/manifest.test.js` fails if one appears.

Merging conflicts on the version line whenever a sibling branch merged first. **Resolve by taking the higher number** — never by reverting a bump.

### Iterating

**Load unpacked** on the worktree folder at `chrome://extensions`. Because the ID is pinned, this **repoints** the one installed presenter at the worktree, keeping its settings — don't remove the existing one first. After a code change, press the reload icon on the extension's card, then reload the page.

If a change seems to have no effect, check the path on the extension's card is the worktree you just edited. The page console logs `presenter <version> loaded from …` on every load; that line is the fastest confirmation you are testing what you think you are.

## Finishing

After the squash-merge, tear down from the primary checkout, then point Chrome back at it:

```bash
cd ~/repos/presenter
# 1. Delete the local branch BEFORE any fetch — under squash merge, -d only passes while the
# stale origin/<branch> ref still exists, and any fetch drops it.
git worktree remove --force ../presenter-<issue>-<slug> || {
# "Directory not empty": Spotlight was reading node_modules. Safe here — the merge happened.
rm -rf ../presenter-<issue>-<slug>
git worktree prune
}
git branch -d <type>/<issue>-<slug>
git fetch --prune

# 2. Update the folder Chrome loads, and reload it.
git pull --ff-only
# then Load unpacked → ~/repos/presenter (or press reload if it already points there)
```

If you already fetched, `-d` fails with *"not fully merged"*: confirm the PR merged (`gh pr view <N>`) and use `-D`. `--force` discards uncommitted changes in the worktree, which is safe only after the merge — never run it out of order.

## Commit scopes

| Scope | Surface |
| --- | --- |
| `presenting` | pointer and highlights |
| `enhancements` | the registry and individual enhancements |
| `popup` | the toolbar popup |
| `content` | the content-script entry point, enhancer and badge |
| `background` | the service worker |
| `repo` | layout, tooling, CI |
| `deps` | dev dependencies |

Always commit `package-lock.json` with `package.json`.

## Code style

`npm run lint` enforces the mechanical part ([`eslint.config.mjs`](eslint.config.mjs)). Beyond it:

- **No `eval`, no `new Function`.** An MV3 extension runs under a CSP that forbids both, so either is a runtime failure on the page; lint enforces it.
- **Keep the browser glue thin.** `src/background.js`, `src/popup/popup.js` and `src/content/main.js` are adapters between `chrome.*` and plain modules; decisions — which page matches, what is active, when the badge shows — live in `src/lib/` and `src/content/` modules that take a `document` and are tested under jsdom. If an adapter needs a comment explaining what it decides, the decision belongs in a module.
- **Match pages by origin, never by substring.** Every enhancement names its pages as Chrome match patterns, evaluated by `src/lib/match.js`; anything else is left alone.
- Two-space indentation, braces on the same line, double quotes, semicolons — as the original code. No formatter.
- JSDoc on exported functions; informative errors, no silent `catch`.

## Review focus areas

Bugs cluster here — scrutinise changes that touch them:

- **Reversibility.** Switching an enhancement off must return the exact shipped page, or the before/after clip is dishonest. `test/enhancer.test.js` and `test/relabel.test.js` compare the DOM before and after; every new enhancement with an `apply` needs the same test.
- **Single-page routing.** Forge and W&B change the URL and content without reloading. The content script re-renders on DOM changes; `apply` runs repeatedly and must be idempotent, or our own edits retrigger the observer in a loop.
- **Selectors against third-party markup.** They break without notice when the site ships. Test against a hand-written fixture in `test/fixtures/`, never a real saved page (see *Public repository*).
- **The manifest.** A content script, popup or module import that does not resolve fails silently in Chrome. `test/manifest.test.js` checks the paths and that every module is a web-accessible resource.
- **Permissions.** Presenter holds `activeTab` and `storage` and injects only on the sites in `manifest.json`. Adding a permission or a target site needs a reason in the PR.

## Documentation

- [`README.md`](README.md) — what it does, install, usage.
- [`CONTEXT.md`](CONTEXT.md) — the glossary, when a change moves the vocabulary.
- [`docs/adr/`](docs/adr/README.md) — a new ADR for a hard-to-reverse decision. ADRs are content-immutable: when one turns out wrong, change its `status` and write a successor. Regenerate the log with `python3 scripts/generate-adr-log.py` (CI checks it is current).

## Checklist

- [ ] Version bumped as the worktree's first commit
- [ ] `npm run lint` and `npm test` pass
- [ ] Tried in Chrome on the worktree build, on the live page
- [ ] Nothing from *Public repository* in the diff, the PR text or the commit messages
- [ ] Docs updated alongside the code
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
# Presenter

Presenter is a simple chrome extension that allows you to draw effemeral rectangles on a page, so you can draw attention to specific areas during a presentation.
Presenter is a Chrome extension with two jobs:

I wrote this extension while working at W&B, so by default, the extension is only enabled on W&B domains.
- **Presenting** — a pointer the audience can follow, and fading rectangles you draw to point at part of a page during a live demo.
- **UX proposals** — switch **enhancements** on and off (a clearer label, a calmer colour) and flip the page between *as shipped* and *proposed*, so one screen recording shows the before and the after.

It started at W&B, so it runs on W&B and CoreWeave Forge pages, plus GitHub and two local dev hosts — the full list is `content_scripts.matches` in [`manifest.json`](manifest.json). Everywhere else it does nothing.

## Installation

1. Clone this repo to your local file system
1. Go to the chrome extensions settings and enable developer mode
1. Click on "Load unpacked" and select the repository folder you just cloned
1. Clone this repo.
1. Open `chrome://extensions` and turn on **Developer mode**.
1. Click **Load unpacked** and select the repo folder.

There is no build step. Upgrading from a version before 1.1.0: remove the old presenter once first — 1.1.0 pins the extension's ID, so Chrome sees it as a new extension ([ADR-0002](docs/adr/0002-pin-the-extension-id.md)).

## Usage
Every time you reload a W&B page, you will get a small gray circle following your pointer. Just press down on the Shift key (turning the circle red/active) while you drag on an area to highlight it with an effemeral rectangle!

Click the presenter icon in the toolbar for its switches. Settings apply to every open tab immediately and are remembered.

### Presenting

- **Pointer & highlights** — a small grey circle follows your pointer. Hold **Shift** (the circle turns red) and drag to draw a rectangle that fades after a few seconds. While Shift is held, clicks draw instead of reaching the page.

On by default. For a readable shared screen, use Chrome's own zoom (`Cmd` `+`), which it remembers per site.

### Recording a UX proposal

1. Open the page and switch on the enhancements you want under **Enhancements** — only those that apply to the current page are listed, each with the problem it addresses.
1. Start recording (Loom, or any screen recorder).
1. Press **Alt+Shift+E** to flip between the shipped page and the proposed one. Change the shortcut at `chrome://extensions/shortcuts`; the popup shows the current one.

While any enhancement is showing, an **Enhanced** badge sits in the top-right corner, so whoever watches the clip knows which version they are seeing. With every enhancement off — or the page flipped to *as shipped* — there is no badge and the page is exactly what shipped.

New enhancements are added as files in [`enhancements/`](enhancements/index.js) — see [`CONTRIBUTING.md`](CONTRIBUTING.md) and [ADR-0001](docs/adr/0001-enhancements-are-versioned-files.md).

## Development

See [`CONTRIBUTING.md`](CONTRIBUTING.md) — including the rules for what may never be committed to this public repo.

```bash
npm install # dev tooling only (Node 24+)
npm run lint
npm test
```
11 changes: 0 additions & 11 deletions background.js

This file was deleted.

Loading
Loading