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
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The two views of a page: **as shipped** is the real product with every enhanceme
_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.
The small "Enhanced" tag at the top centre of the page, 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
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Ordinary git and GitHub practice — opening an issue, naming a branch, writing
**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.
- **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`, counting up in the last two digits when a fixture needs several). `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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ On by default. For a readable shared screen, use Chrome's own zoom (`Cmd` `+`),
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.
While any enhancement is showing, an **Enhanced** badge sits at the top centre of the page, 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).

Expand Down
63 changes: 63 additions & 0 deletions enhancements/forge-sandbox-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Ended sandboxes in the Forge list read as a grey "Terminated" instead of a green "Completed".
*
* Targets data- attributes only: Forge's class names are build-hashed and change between
* releases. The grey reuses the pill's own colour variables with Forge's neutral values, so
* the proposed state looks native; fallbacks cover a page where those variables are missing.
*
* The overrides are !important because Forge raises its colour rules' specificity by
* repeating the class (`._green_x._green_x._green_x`), which outranks any plain selector here.
* They only exist while the enhancement is on, so the shipped page is untouched when off.
*/

import { createRelabel } from "../src/lib/relabel.js";

const CLASS = "presenter-sandbox-ended";

// A ring with a short horizontal dash, like the European no-entry sign — calmer than a
// diagonal bar, and read as "ended" rather than "error". Drawn as a mask so it takes
// the pill's text colour, at the same 24-unit grid and size as the checkmark it replaces.
const ENDED_ICON = encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
'<circle cx="12" cy="12" r="9" fill="none" stroke="black" stroke-width="2"/>' +
'<path d="M8 12h8" stroke="black" stroke-width="2" stroke-linecap="round"/>' +
"</svg>",
);

const relabel = createRelabel({
selector: 'td[data-key$=":status"] [data-component="Pill"]',
from: "Completed",
to: "Terminated",
className: CLASS,
});

export const forgeSandboxStatus = {
id: "forge-sandbox-status",
title: "Ended sandboxes read as grey Terminated",
problem:
"An ended sandbox shows a green Completed, which reads as healthy or running; it cannot be started again, so Terminated in neutral grey says what it is.",
matches: ["https://forge.coreweave.com/sandboxes/*"],
css: `
[data-component="Pill"].${CLASS} {
--tag-text-color: var(--secondary-text, #5f6368) !important;
--tag-background-color: light-dark(var(--moon-200, #e8eaed), var(--moon-800, #3c4043)) !important;
}
[data-component="Pill"].${CLASS} [data-icon] {
--icon-color: var(--secondary-text, #5f6368) !important;
position: relative;
}
[data-component="Pill"].${CLASS} [data-icon] svg {
visibility: hidden;
}
[data-component="Pill"].${CLASS} [data-icon]::before {
content: "";
position: absolute;
inset: 0;
background-color: var(--icon-color);
-webkit-mask: url("data:image/svg+xml,${ENDED_ICON}") center / contain no-repeat;
mask: url("data:image/svg+xml,${ENDED_ICON}") center / contain no-repeat;
}
`,
apply: relabel.apply,
revert: relabel.revert,
};
4 changes: 3 additions & 1 deletion enhancements/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
* Add the new file's export to the list below.
*/

export const ENHANCEMENTS = [];
import { forgeSandboxStatus } from "./forge-sandbox-status.js";

export const ENHANCEMENTS = [forgeSandboxStatus];
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "presenter",
"version": "1.1.0",
"version": "1.2.0",
"description": "Pointer and fading highlights for live demos, plus toggleable UX enhancements for before/after clips.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzSA8AU/9KdGceK/m1e9ke5Gi7B4HObymgejDydm3gAlJXrqcAys1TV5HsphZF/XG4ZNew/Ui2EzRGpJ3ryN9vP90EklOAgHIT4QLSL8zwTarwubELnoLHhqDFtxX/xe8fBb7zQsbXcZbcPHHlj0Fm2qkZgWHjrR1M+6/YzrxkFWOl0tgPibihEarn33hCCBvej/8AXDjF0JEV+farFxSc1f7jVNAgSe7O976GKlrXY6mYurJnXGv6G4EhPG1VGrsrVY/HcKHR3dotAcgp4qPIMHSNOfScxc75UWUKJarqCXKlX5NL9H3Vb84MmMwpOvo01SpDJZc/ZQRrxPOtMCRzQIDAQAB",
"permissions": [
Expand Down
4 changes: 2 additions & 2 deletions src/content/badge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* The Enhanced badge: a small top-right tag shown only while the page differs from what
* shipped. Top-right because Loom's camera bubble sits bottom-left by default.
* The Enhanced badge: a small top-centre tag shown only while the page differs from what
* shipped. At the top because Loom's camera bubble sits bottom-left by default.
*/

export const BADGE_ID = "presenter-enhanced-badge";
Expand Down
5 changes: 3 additions & 2 deletions src/content/presenting.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@
}


/* The Enhanced badge — top-right, clear of Loom's default bottom-left camera bubble */
/* The Enhanced badge — top centre, clear of Loom's default bottom-left camera bubble */
#presenter-enhanced-badge {
position: fixed;
top: 12px;
right: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 2147483647;
padding: 4px 10px;
border-radius: 999px;
Expand Down
28 changes: 21 additions & 7 deletions src/lib/relabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
* A reversible text relabel — the building block for "say it differently" enhancements.
*
* Elements matched by `selector` whose trimmed text equals `from` get `to` as their text and
* `className` added. The original child nodes are kept (icons included) and put back by
* `revert`, so the restore is exact. An element the page re-rendered in the meantime is a new
* node that never carried our edit, so there is nothing to restore on it.
* `className` added. Only the element's own text nodes are rewritten — child elements such as
* an icon stay in place, and so do the text nodes themselves, which matters on a page
* rendered by a framework that keeps references to them. `revert` puts each node's original
* text back, so the restore is exact. An element the page re-rendered in the meantime is a
* new node that never carried our edit, so there is nothing to restore on it.
*
* `apply` skips elements already showing the new label, so it is idempotent.
*/
Expand All @@ -28,19 +30,29 @@ export function createRelabel({ selector, from, to, className }) {
for (const element of doc.querySelectorAll(selector)) {
// Already relabelled and still showing our text: nothing to do. If the page wrote over
// our text in place (a framework re-render), fall through and relabel the new content.
if (element.hasAttribute(RELABELLED) && element.textContent === to) {
if (element.hasAttribute(RELABELLED) && element.textContent.trim() === to) {
continue;
}
if (element.textContent.trim().toLowerCase() !== target) {
continue;
}
originals.set(element, [...element.childNodes]);
const textNodes = [...element.childNodes].filter(
(node) => node.nodeType === node.TEXT_NODE && node.data.trim() !== "",
);
if (textNodes.length === 0) {
// The text sits deeper than this element; relabelling it would mean replacing child
// elements, which would not be reversible against a live framework.
continue;
}
originals.set(element, textNodes.map((node) => [node, node.data]));
element.setAttribute(RELABELLED, "");
if (className && !element.classList.contains(className)) {
element.classList.add(className);
element.setAttribute(ADDED_CLASS, className);
}
element.textContent = to;
textNodes.forEach((node, index) => {
node.data = index === 0 ? node.data.replace(node.data.trim(), to) : "";
});
}
};

Expand All @@ -49,7 +61,9 @@ export function createRelabel({ selector, from, to, className }) {
if (!originals.has(element)) {
continue;
}
element.replaceChildren(...originals.get(element));
for (const [node, data] of originals.get(element)) {
node.data = data;
}
originals.delete(element);
element.removeAttribute(RELABELLED);
const added = element.getAttribute(ADDED_CLASS);
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures-hygiene.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const fixtures = join(import.meta.dirname, "fixtures");
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
// Synthetic placeholders that are allowed through.
const ALLOWED = [/@example\.(com|org)\b/, /\b0{8}-0{4}-0{4}-0{4}-0{12}\b/];
const ALLOWED = [/@example\.(com|org)\b/, /\b0{8}-0{4}-0{4}-0{4}-0{10}\d{2}\b/];

function committedFixtures() {
if (!existsSync(fixtures)) {
Expand All @@ -36,5 +36,6 @@ describe("committed fixtures", () => {
expect(scrub("contact jane@acme.test")).toMatch(EMAIL);
expect(scrub("sandbox 12345678-abcd-4ef0-9abc-1234567890ab")).toMatch(UUID);
expect(scrub("someone@example.com 00000000-0000-0000-0000-000000000000")).not.toMatch(EMAIL);
expect(scrub("00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000042")).not.toMatch(UUID);
});
});
22 changes: 22 additions & 0 deletions test/fixtures/forge-sandbox-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!--
Hand-written, synthetic: the structure of the Forge sandboxes list's status cells, with
placeholder IDs and hashed class names shortened to their shape. Not a saved page — see
CONTRIBUTING.md, "Public repository".
-->
<table role="grid">
<tbody>
<tr role="row">
<td role="rowheader" data-key="00000000-0000-0000-0000-000000000000:id"><a href="/sandboxes/example-sandboxes/00000000-0000-0000-0000-000000000000">00000000-0000-0000-0000-000000000000</a></td>
<td role="gridcell" data-key="00000000-0000-0000-0000-000000000000:status"><span class="_green_hash_51 _pill_hash_1" data-component="Pill"><span aria-hidden="true" data-icon="checkmark-circle" class="_baseIcon_hash_21 _xs_hash_1 _green_hash_78"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20"></path></svg></span>Completed</span></td>
<td role="gridcell" data-key="00000000-0000-0000-0000-000000000000:created"><time datetime="2026-01-01T00:00:00.000Z">1 day ago</time></td>
</tr>
<tr role="row">
<td role="rowheader" data-key="00000000-0000-0000-0000-000000000001:id"><a href="/sandboxes/example-sandboxes/00000000-0000-0000-0000-000000000001">00000000-0000-0000-0000-000000000001</a></td>
<td role="gridcell" data-key="00000000-0000-0000-0000-000000000001:status"><span class="_teal_hash_41 _pill_hash_1" data-component="Pill"><span aria-hidden="true" data-icon="loading" class="_baseIcon_hash_21 _xs_hash_1"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2v4"></path></svg></span>Running</span></td>
<td role="gridcell" data-key="00000000-0000-0000-0000-000000000001:created"><time datetime="2026-01-01T00:00:00.000Z">1 day ago</time></td>
</tr>
<tr role="row">
<td role="gridcell" data-key="00000000-0000-0000-0000-000000000001:displayName"><span data-component="Pill">Completed</span></td>
</tr>
</tbody>
</table>
57 changes: 57 additions & 0 deletions test/forge-sandbox-status.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @vitest-environment jsdom
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import { createEnhancer } from "../src/content/enhancer.js";
import { forgeSandboxStatus } from "../enhancements/forge-sandbox-status.js";
import { urlMatches } from "../src/lib/match.js";

const fixture = readFileSync(join(import.meta.dirname, "fixtures", "forge-sandbox-list.html"), "utf8");
const statusPills = () => [...document.querySelectorAll('td[data-key$=":status"] [data-component="Pill"]')];

describe("forge-sandbox-status", () => {
let original;

beforeEach(() => {
document.head.innerHTML = "";
document.body.innerHTML = fixture;
original = document.documentElement.outerHTML;
});

it("targets the sandboxes pages only", () => {
expect(urlMatches("https://forge.coreweave.com/sandboxes/example-sandboxes", forgeSandboxStatus.matches)).toBe(true);
expect(urlMatches("https://forge.coreweave.com/notebooks", forgeSandboxStatus.matches)).toBe(false);
});

it("relabels a Completed status pill and keeps its icon", () => {
const [ended] = statusPills();
const icon = ended.querySelector("[data-icon]");
forgeSandboxStatus.apply(document);
expect(ended.textContent.trim()).toBe("Terminated");
expect(ended.classList.contains("presenter-sandbox-ended")).toBe(true);
expect(ended.querySelector("[data-icon]")).toBe(icon);
});

it("leaves other statuses and Completed pills outside the status column alone", () => {
forgeSandboxStatus.apply(document);
const [, running] = statusPills();
expect(running.textContent.trim()).toBe("Running");
expect(document.querySelector('[data-key$=":displayName"] [data-component="Pill"]').textContent).toBe("Completed");
});

it("restores the shipped page exactly when switched off", () => {
const enhancer = createEnhancer(document);
enhancer.sync([forgeSandboxStatus]);
expect(document.documentElement.outerHTML).not.toBe(original);
enhancer.sync([]);
expect(document.documentElement.outerHTML).toBe(original);
});

it("is idempotent across repeated syncs", () => {
const enhancer = createEnhancer(document);
enhancer.sync([forgeSandboxStatus]);
const once = document.documentElement.outerHTML;
enhancer.sync([forgeSandboxStatus]);
expect(document.documentElement.outerHTML).toBe(once);
});
});
22 changes: 16 additions & 6 deletions test/relabel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,29 @@ describe("createRelabel", () => {
it("relabels only matching elements with matching text", () => {
relabel.apply(document);
const [ended, running] = document.querySelectorAll(".status");
expect(ended.textContent).toBe("Stopped");
expect(ended.textContent.trim()).toBe("Stopped");
expect(ended.classList.contains("is-ended")).toBe(true);
expect(ended.querySelector(".icon")).not.toBeNull();
expect(running.textContent).toBe("running");
expect(document.querySelector(".other").textContent).toBe("completed");
});

it("restores the page exactly, child nodes included", () => {
it("restores the page exactly, keeping the same nodes", () => {
const original = document.body.innerHTML;
const icon = document.querySelector(".icon");
const ended = document.querySelector(".status");
const nodes = [...ended.childNodes];
relabel.apply(document);
expect([...ended.childNodes]).toEqual(nodes);
relabel.revert(document);
expect(document.body.innerHTML).toBe(original);
expect(document.querySelector(".icon")).toBe(icon);
expect([...ended.childNodes]).toEqual(nodes);
});

it("leaves an element alone when its text is nested deeper", () => {
document.body.innerHTML = `<span class="status"><b>completed</b></span>`;
const original = document.body.innerHTML;
relabel.apply(document);
expect(document.body.innerHTML).toBe(original);
});

it("is idempotent", () => {
Expand All @@ -48,8 +58,8 @@ describe("createRelabel", () => {
it("relabels again when the page writes over our text in place", () => {
relabel.apply(document);
const ended = document.querySelector(".status");
ended.textContent = "completed";
ended.lastChild.data = "Completed";
relabel.apply(document);
expect(ended.textContent).toBe("Stopped");
expect(ended.textContent.trim()).toBe("Stopped");
});
});
Loading