From the review of the parser change: scripts typed text/ecmascript,
application/x-javascript and the other JavaScript MIME types run as classic
scripts too, so they're checked now. And the duplicate-name check (V8 only)
no longer skips with the var check when a Node lacks acorn.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fix review showed the var regex flagged the word in comments and strings and
missed var/*c*/x and var {x}. The test now uses the acorn parser Node ships
(--expose-internals; it skips if a Node ever drops it) to find real var
declarations, and reads the page with html.parser so it checks inline classic
scripts only: no src, and no module or JSON data type.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the fix review:
- The hero's Install skipped installing whenever a job existed, including one
that had failed, so it only opened the details. It now tries again after a
failure, and still only opens the details while installing or once installed.
- Declaring a var twice is legal, so the strict-mode check couldn't see it. The
page's one var (a block-local in the capture code) is now let, and the test
rejects var outright (CSS var() in strings is fine). It also checks every
inline script, whatever its attributes.
- Clear filters still set the removed source filter.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fix review found the Games reload added to showPage() read `link`, which
the page declares later: opening Frame Control at #games, #library,
#sideloaded or #getgames threw at startup and skipped the rest of the setup.
And the iPhone app never watches the connection, so `link.s` stayed null and
titles still never reloaded there.
The reload now runs only when navigating to Games (the hashchange), where the
first load is already done, and doesn't depend on connection state; a failed
load shows its error in the list as Refresh did. tests/test_page_startup.py runs
the real showPage() at every page and section link with everything declared
later still uninitialised, and fails on the old line.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the independent review of this PR:
- After installing the featured app, the hero's Install on the Frame took the
installed branch and launched it on the headset. It now opens the details,
whose button says Open in Steam, without acting.
- The duplicate-name test matched declarations at column zero only, so an
indented second loadPanels (the bug it was written for) passed, and so did
classes, destructuring and later declarators. It now compiles both inline
scripts as one strict-mode block, where V8 rejects any name declared twice
in the shared scope, with fixtures for each case and for same-named helpers
in different functions (which are fine).
- A card's "Won't run" overstated the check's "Might not work"; it now says
May not run.
- Removed styles and the source-filter state that nothing used any more.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the independent review of this PR:
- Phones lost the Refresh button, and nothing else reloaded sideloaded titles,
so one added or removed from another computer never showed. Opening Games
now reloads them.
- Send files... on Home uploaded with its progress bar in the hidden Tools
page, and phones have no Activity bar. Choosing files from Home now opens
the file panel on Tools.
- A session that finished while the page was open left its settings (1 / 0 / 0
after a test) in the form. The form goes back to its defaults when the
session ends. The test now covers that, and its mock uses the page's real
break default (20 minutes, not 30).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With one more button in the header the pill lost its route at a few widths
(970-1020, 1160-1180 and 1350-1410 px). The header's spacing now tightens below
1500 px, the shortcut hints go at 1200, and the compact header starts at 1030,
so the pill reads "Connected · Tailscale" in full at every width above that,
including the 1400 px default window.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Apps without a banner or screenshot show their icon on a blurred wash of
itself, or a VR or app glyph, rather than a giant letter. Every card says
whether it will run on the Frame (works, should install, not checked yet,
won't run), in place of "Free" on every card.
The featured app isn't repeated in the rows below, and no row repeats
another. Its buttons stay in the app: Install on the Frame opens the details
with the install under way, and Details opens them. It's chosen from apps
that can run, with real artwork, VR first.
The stray "Browse SideQuest" link goes; the Sources dialog lists SideQuest
with a link to its website instead of a switch that did nothing. The "All
sources" drop-down goes too, as the Sources dialog already chooses them.
The copy is plain and British ("Apps and games for the Frame", no "Find
your next favorite"), and Games and Android point at each other.
Two functions were each declared twice across the page's scripts, so the
later one silently replaced the earlier everywhere: the store's details
dialog opened empty (the Devices tab's renderDetail ran instead), and Home's
Desktop view never found the headset's panels (the panel switcher's loader
ran instead). They're renamed, and a test
fails on any top-level name declared twice.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Home answers "what's my headset doing, and what do I want to do now". A
Right now card says whether SteamVR is running and holds the volume and the
next things people do: cast to this screen, play a video or photo, show a
Mac window, send files, type on the Frame. With nothing captured, the
headset view is a short strip rather than a big black box; the keyboard and
trackpad fold to one line until turned on; VR performance folds away.
Screenshots and Family and comfort follow.
Tools is grouped: in the headset (Mac in the headset, the media player, the
panel switcher), then sending files, Linux apps, remote and power. Privacy
and updates, library artwork and the assistant move to a Settings page,
opened by a gear in the header (key 6), with About and licences.
Family and comfort no longer fills the form with a finished session's
settings, so it shows 30 / 20 / 30 rather than the last test's 1 / 0 / 0,
and its recent events carry their times.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:48:57 +10:00
14 changed files with 569 additions and 188 deletions
<ahref="#home"class="on"title="Headset view, status and screenshots (1)"><svgviewBox="0 0 24 24"fill="none"stroke="currentColor"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"aria-hidden="true"><rectx="2.5"y="7"width="19"height="10"rx="4"/><circlecx="8.5"cy="12"r="1.6"/><circlecx="15.5"cy="12"r="1.6"/></svg>Home<kbd>1</kbd></a>
<buttonclass="action"id="castBtn"title="The live headset view, full screen, for people in the room">Cast to this screen</button>
<buttondata-go="media">Play a video or photo</button>
<buttondata-go="macview"id="nowMac"hidden>Show a Mac window</button>
<buttonid="nowSend">Send files…</button>
<buttondata-go="pad">Type on the Frame</button>
</div>
<pclass="hint">Casting shows everything the wearer sees, including anything private.</p>
</section>
</div>
<sectionclass="panel"id="vrTools">
<divclass="shelf-head"><h2>VR comfort and performance</h2><spanclass="spacer"></span><spanclass="sub"id="vrUpdated">Waiting for the Frame</span></div>
<divclass="stats"id="vrMetrics"></div>
<divclass="hint">Compositor FPS measures SteamVR output, not game FPS. Application FPS is unavailable when no app supplies timing. GPU time is render time, not GPU utilisation. Battery and temperature are in the battery menu at the top.</div>
<divclass="chips">
<buttonclass="small"id="hudStart">Open HUD in headset</button>
<buttonclass="small"id="shotsFolder"title="Open the SteamFrame folder in your Pictures">Show folder</button>
<buttonclass="action small"id="shotsSaveNew"disabled>Save new to this computer</button>
</div>
<divclass="hint">The HUD is ours and uses the Frame’s built-in tools. Place its panel with SteamVR’s Float in World or controller docking controls. It updates every two seconds until closed.</div>
<h3>Playspace and seated comfort</h3>
<pclass="sub"id="comfortNote">Tracking-origin controls are not available yet: reversible changes still need an idle-headset test. Use SteamVR’s recenter and room setup for now. A seated origin only affects apps that use seated tracking; it cannot force a game into seated mode.</p>
<divclass="hint">For motion comfort, choose snap-turn, teleport movement and a movement vignette in each game’s settings. SteamVR has no verified universal snap-turn or locomotion-vignette switch.</div>
<pclass="sub">None of Frame Control’s features needs these apps. Compatibility reports and account ownership are separate. Paid apps can only be installed here when already owned.</p>
<buttonclass="small"id="vrUtilitiesRefresh">Check ownership and reports</button>
<divclass="list"id="vrUtilities"><divclass="sub">Load this optional list to check the Frame account.</div></div>
<divclass="hint">Screenshots you take in the headset with Steam's screenshot shortcut. Click one to open it in the viewer; Save copies it to <code>~/Pictures/SteamFrame</code>.</div>
<buttonclass="small"id="shotsFolder"title="Open the SteamFrame folder in your Pictures">Show folder</button>
<buttonclass="action small"id="shotsSaveNew"disabled>Save new to this computer</button>
<detailsclass="panel fold"id="vrTools">
<summaryclass="shelf-head"><h2>VR performance</h2><spanclass="count">Frame timing, the HUD, playspace and optional software</span>
<spanclass="spacer"></span><spanclass="sub"id="vrUpdated">Waiting for the Frame</span></summary>
<divclass="stats"id="vrMetrics"></div>
<divclass="hint">Compositor FPS measures SteamVR output, not game FPS. Application FPS is unavailable when no app supplies timing. GPU time is render time, not GPU utilisation. Battery and temperature are in the battery menu at the top.</div>
<divclass="chips">
<buttonclass="small"id="hudStart">Open HUD in headset</button>
<divclass="hint">Screenshots you take in the headset with Steam's screenshot shortcut. Click one to open it in the viewer; Save copies it to <code>~/Pictures/SteamFrame</code>.</div>
</section>
<divclass="hint">The HUD is ours and uses the Frame’s built-in tools. Place its panel with SteamVR’s Float in World or controller docking controls. It updates every two seconds until closed.</div>
<h3>Playspace and seated comfort</h3>
<pclass="sub"id="comfortNote">Tracking-origin controls are not available yet: reversible changes still need an idle-headset test. Use SteamVR’s recenter and room setup for now. A seated origin only affects apps that use seated tracking; it cannot force a game into seated mode.</p>
<divclass="hint">For motion comfort, choose snap-turn, teleport movement and a movement vignette in each game’s settings. SteamVR has no verified universal snap-turn or locomotion-vignette switch.</div>
<pclass="sub">None of Frame Control’s features needs these apps. Compatibility reports and account ownership are separate. Paid apps can only be installed here when already owned.</p>
<buttonclass="small"id="vrUtilitiesRefresh">Check ownership and reports</button>
<divclass="list"id="vrUtilities"><divclass="sub">Load this optional list to check the Frame account.</div></div>
<pclass="sub cross-link">Your Steam library and the Steam store. Free VR games and apps from outside Steam are on
<ahref="#android">Android</a>.</p>
<divclass="panel">
<divclass="cat-tools">
<divclass="seg"id="gmSrc"aria-label="Where to look">
@@ -953,11 +1006,11 @@
<divclass="page"data-page="android">
<sectionid="android">
<divclass="store-tabs"aria-label="Android sections"><buttonclass="on"data-store-tab="browse"aria-pressed="true">Discover</button><buttondata-store-tab="library"aria-pressed="false">On your Frame <spanid="andCount"></span></button><spanclass="spacer"></span><buttonid="sourcesOpen"class="small">Sources <spanaria-hidden="true">⚙</span></button><buttonclass="small"id="repNew"hidden>+ Report an APK</button></div>
<divclass="store-tabs"aria-label="Android sections"><buttonclass="on"data-store-tab="browse"aria-pressed="true">Discover</button><buttondata-store-tab="library"aria-pressed="false">On your Frame <spanid="andCount"></span></button><spanclass="spacer"></span><buttonid="sourcesOpen"class="small"title="Choose where Discover looks for apps">Sources…</button><buttonclass="small"id="repNew"hidden>+ Report an APK</button></div>
<sectionid="appStore"aria-label="Discover apps">
<divclass="store-toolbar"><divclass="store-heading"><div><divclass="store-eyebrow">A little more possibility</div><h1>Find your next favorite.</h1><p>Great games and useful apps. All in one place, all free to explore.</p></div></div>
<divclass="store-toolbar"><divclass="store-heading"><div><h1>Apps and games for the Frame</h1><p>Free Android apps and VR games from open app stores and developers' own pages, each marked with whether it runs on the Frame. Steam games are in <ahref="#games">Games</a>.</p></div></div>
<formid="sourceSearch"class="store-search"><svgviewBox="0 0 24 24"fill="none"stroke="currentColor"stroke-width="1.8"aria-hidden="true"><circlecx="10.5"cy="10.5"r="6.5"/><pathd="m16 16 5 5"/></svg><inputid="sourceQ"type="search"aria-label="Search games and apps"placeholder="Search games and apps"><buttontype="submit"class="small">Search</button></form></div>
<divclass="store-filters"><divid="searchFilters"class="store-chips"><buttondata-filter="all"aria-pressed="true">For you</button><buttondata-filter="vr"aria-pressed="false">VR experiences</button><buttondata-filter="flat"aria-pressed="false">Flat-screen apps</button><buttondata-filter="installable"aria-pressed="false">Fits your Frame</button></div><labelclass="store-source-select"><spanclass="sr-only">Filter by source</span><selectid="sourceFilter"><optionvalue="">All sources</option></select></label></div>
<divclass="store-filters"><divid="searchFilters"class="store-chips"><buttondata-filter="all"aria-pressed="true">All</button><buttondata-filter="vr"aria-pressed="false">VR</button><buttondata-filter="flat"aria-pressed="false">Flat-screen</button><buttondata-filter="installable"aria-pressed="false">Installs on the Frame</button></div></div>
<sectionclass="panel"><h2>Assistant and AI agents</h2><p>Use your own model endpoint, or review a proposed MCP action. Nothing is sent to a model until you opt in.</p><ahref="/assistant">Open assistant</a></section>
<divclass="shelf-head group-head"><h2>In the headset</h2><spanclass="count">Show your computer, films and photos, and panels</span></div>
<divclass="grid-3">
<sectionclass="panel"id="macview"hidden>
<divclass="shelf-head"><h2>Mac in the headset</h2><spanclass="count"id="mvCount"></span><spanclass="spacer"></span>
<pclass="sub">SteamGridDB supplies community artwork for sideloaded games. Without a key, Frame Control uses source images and creates the remaining art.</p>
<labelfor="steamGridKey">SteamGridDB API key</label>
<inputtype="password"id="steamGridKey"autocomplete="off"spellcheck="false"placeholder="Optional API key">
<buttonid="refreshAndroidArt"class="small"style="margin-top:12px">Refresh artwork for installed apps and titles</button>
</section>
<sectionclass="panel"id="power">
<divclass="shelf-head"><h2>Remote & power</h2></div>
<divclass="actions">
@@ -1120,45 +1192,17 @@
<divclass="hint desk-only">Sleep, Restart and Shut down open a terminal window for the Developer Mode password.</div>
<divclass="hint mobile-only">Sleep, Restart and Shut down ask for the Developer Mode password. SSH, SFTP, Steam Link and remote desktop open in the app that handles them.</div>
<pclass="sub">SteamGridDB supplies community artwork for sideloaded games. Without a key, Frame Control uses source images and creates the remaining art.</p>
<labelfor="steamGridKey">SteamGridDB API key</label>
<inputtype="password"id="steamGridKey"autocomplete="off"spellcheck="false"placeholder="Optional API key">
<divclass="sources-intro"><divclass="store-eyebrow">Your app shelves</div><h1id="sourcesTitle">A world of apps.</h1><p>Choose where you discover games and apps. We bring them together for you.</p></div>
<divclass="sources-intro"><h1id="sourcesTitle">Sources</h1><p>Where Discover looks for apps. Turn a source off to hide its apps.</p></div>
<divid="sourcesList"></div>
<formid="addSource"class="source-repo-form"><h2>Add a source</h2><p>Have a favorite F-Droid repository? Add its address to browse its apps here.</p><label>Repository address<inputname="url"type="url"requiredplaceholder="https://example.org/fdroid/repo"></label><details><summary>Verify this source (optional)</summary><label>Repository fingerprint<inputname="fingerprint"type="text"autocomplete="off"placeholder="Paste the fingerprint from the repository owner"></label></details><buttonclass="action">Add source</button></form>
<formid="addSource"class="source-repo-form"><h2>Add a source</h2><p>Add an F-Droid repository by its address to browse its apps here.</p><label>Repository address<inputname="url"type="url"requiredplaceholder="https://example.org/fdroid/repo"></label><details><summary>Verify this source (optional)</summary><label>Repository fingerprint<inputname="fingerprint"type="text"autocomplete="off"placeholder="Paste the fingerprint from the repository owner"></label></details><buttonclass="action">Add source</button></form>
return `<spanclass="store-art ${cls}${image ? "":"no-image"}"style="--hue:${storeHue(o.name)}">${under}${storeImage(image, alt || `${o.name} artwork`, eager)}</span>`;
}
function storeIcon(o) { return `<spanclass="store-icon"style="--hue:${storeHue(o.name)}"aria-hidden="true">${esc(storeInitial(o.name))}${storeImage(o.artwork?.icon, "")}</span>`; }
document.addEventListener("error", e => { if (e.target.matches?.(".store-art img, .store-icon img")) e.target.remove(); }, true);
document.addEventListener("error", e => {
if (!e.target.matches?.(".store-art img, .store-icon img")) return;
const art = e.target.closest(".store-art");
e.target.remove();
// Nothing left to show: the glyph rather than an empty wash.
if (art && !art.querySelector("img") && !art.querySelector(".art-glyph")) art.insertAdjacentHTML("afterbegin", `<spanclass="art-glyph">${GLYPH_APP}</span>`);
}, true);
function sourceMark(s) { const name=storeName(s); return `<spanclass="offer-mark"style="--hue:${storeHue(name)}"aria-hidden="true">${esc(name === "SideQuest" ? "SQ" : name === "F-Droid" ? "Fd" : name.slice(0,2))}</span>`; }
function sourceDescription(s) { return s.description || ({fdroid:"Independent, open-source apps",sidequest:"Discover something new in VR",itch:"Games from independent creators",github:"Direct from the people who make it"})[s.kind] || (s.name.includes("GitHub") ? "Direct from the people who make it" : s.name.includes("Droid") ? "Independent, open-source apps" : s.name.includes("SideQuest") ? "Discover something new in VR" : s.name.includes("itch") ? "Games from independent creators" : "Apps shared by this community"); }
function sourceDescription(s) {
const kinds = {fdroid:"Free and open-source Android apps", itch:"Games from independent developers", github:"Releases straight from the developers",
sidequest:"VR games and apps. Its terms don't allow searching it from here, so browse it on its website."};
return s.description || kinds[s.kind] || "Apps from this repository";
$("sourcesList").innerHTML = sources.map(s=>`<divclass="source-setting">${sourceMark(s.name)}<divclass="grow"><strong>${esc(storeName(s.name))}</strong><p>${esc(sourceDescription(s))}</p><small>${esc(({official:"Official store",community:"Community",user:"Added by you"})[s.trust] || "Community")}</small>${s.builtin === false ? `<buttonclass="small"data-remove="${esc(s.id)}"aria-label="Remove ${esc(s.name)}">Remove</button>` : ""}</div><inputclass="source-toggle"role="switch"type="checkbox"aria-label="Enable ${esc(storeName(s.name))}"data-enable="${esc(s.id)}"${s.enabled?"checked":""}></div>`).join("") || `<pclass="store-notice">Your sources will appear here when they are available.</p>`;
$("sourcesList").innerHTML = sources.map(s=>`<divclass="source-setting">${sourceMark(s.name)}<divclass="grow"><strong>${esc(storeName(s.name))}</strong><p>${esc(sourceDescription(s))}</p><small>${esc(({official:"Official store",community:"Community",user:"Added by you"})[s.trust] || "Community")}</small>${s.builtin === false ? `<buttonclass="small"data-remove="${esc(s.id)}"aria-label="Remove ${esc(s.name)}">Remove</button>` : ""}</div>${s.page_only ? `<aclass="source-web"href="${esc(sourcePage(s.url))}"target="_blank"rel="noopener"title="Its apps can't be searched from here">Website ↗</a>` : `<inputclass="source-toggle"role="switch"type="checkbox"aria-label="Enable ${esc(storeName(s.name))}"data-enable="${esc(s.id)}"${s.enabled?"checked":""}>`}</div>`).join("") || `<pclass="store-notice">Your sources will appear here when they are available.</p>`;
return `<divclass="store-empty"><svgviewBox="0 0 120 100"fill="none"aria-hidden="true"><rectx="18"y="25"width="60"height="55"rx="12"fill="currentColor"opacity=".08"/><rectx="29"y="18"width="60"height="55"rx="12"stroke="currentColor"opacity=".35"/><circlecx="61"cy="43"r="15"stroke="currentColor"stroke-width="3"/><pathd="m72 55 17 18M17 12v10m-5-5h10m79-4v10m-5-5h10"stroke="currentColor"stroke-width="3"stroke-linecap="round"/></svg><h2>A little further to explore.</h2><p>No apps found this time. Try a different name, clear your filters, or explore another source.</p><buttonclass="action"id="storeReset">Explore all apps</button></div>`;
$("searchGrid").innerHTML=`<buttonclass="store-hero"data-app="${apps.indexOf(featured)}"aria-label="Explore ${esc(featured.name)}">${storeArt(o,"",true)}<spanclass="hero-copy"><spanclass="store-eyebrow">Step into something new</span><strong>${esc(featured.name)}</strong><span>${esc(storeText(featured.summary))}</span><spanclass="hero-cta">Explore ${esc(featured.name)} <spanaria-hidden="true"> ↗</span></span></span></button>` + storeRow(apps.some(a=>a.offers[0].popularity) ? "Popular apps" : "Explore apps","More ways to make your Frame yours",popular) + storeRow(tested.length ? "VR that works on the Frame" : "Step into VR",tested.length ? "Experiences checked on a Frame" : "A new perspective, a whole new playground",vr) + storeRow("Recently updated","Fresh from their creators",recent);
// The featuredapp: something that runs, with real artwork, and preferably VR. It isn't repeated below.
$("searchGrid").innerHTML=`<divclass="store-hero"data-app="${apps.indexOf(featured)}">${storeArt(o,"",true)}<divclass="hero-copy"><spanclass="store-eyebrow">Featured${o.vr ? " in VR" : ""}</span><strong>${esc(featured.name)}</strong><spanclass="hero-sum">${esc(storeText(featured.summary))}</span><spanclass="hero-runs">${storeRuns(o)}</span><spanclass="hero-actions">${canInstall ? `<buttonclass="action"data-hero-install="${apps.indexOf(featured)}">Install on the Frame</button>` : ""}<buttonclass="hero-details"data-app="${apps.indexOf(featured)}">Details</button></span></div></div>` +
storeRow(tested.length ? "VR that works on the Frame" : "VR games and experiences", tested.length ? "Checked on a Frame" : "Made for headsets; most aren't checked on the Frame yet", vrRow) +
storeRow(apps.some(a=>a.offers[0].popularity) ? "Popular" : "More to try", "", popular) +
storeRow("Recently updated", "New versions from their developers", recent);
} catch(e) { if(request===sourceState.request) { $("searchGrid").innerHTML=storeEmpty();$("sourceStatus").hidden=false;$("sourceStatus").textContent="We couldn't reach the app shelves. Try again in a moment."; } }
} catch(e) { if(request===sourceState.request) { $("searchGrid").innerHTML=storeEmpty("Couldn't reach the app sources. Try again in a moment.");$("sourceStatus").hidden=true; } }
$("detailInstallNote").textContent=job?.result?.game_data ? (job.dataError || 'Installed. It also needs its game data: open it once on the Frame, then choose Add game data.') : job?.result ? '✓ Installed. Find it in your Steam library.' : job?.error || (job && !job.done ? 'Keep exploring. We’ll take care of the installation.' : o.verdict?.tone==='works' ? 'Adds to your Steam library.' : 'Compatibility can vary. Adds to your Steam library.');
$("detailInstallNote").textContent=job?.result?.game_data ? (job.dataError || 'Installed. It also needs its game data: open it once on the Frame, then choose Add game data.') : job?.result ? '✓ Installed. Find it in your Steam library.' : job?.error || (job && !job.done ? 'Installing. You can close this and keep browsing.' : o.verdict?.tone==='works' ? 'Adds to your Steam library.' : 'Compatibility can vary. Adds to your Steam library.');
} catch(e) { if(d===sourceState.detail)$("detailInstallNote").textContent="Some app details couldn't be refreshed. You can still view this listing."; }
}
async function installStoreApp() {
@@ -3230,17 +3357,16 @@ async function installStoreApp() {
}
async function changeSource(body) {
$("sourceError").textContent="Saving your sources…";
let devicesSeq = 0; // an older answer arriving late mustn't bring back the old selection
async function loadDevices() {
if (!link.live) return;
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.