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>
Three review rounds kept finding ways Use now could point at an address a
reconnect wouldn't pick: the page was predicting the outcome of the server's
race from test results that could be unfinished, from another network, or out
of date after a reorder or a Tailscale change. Adding the offered LAN address
first in the list is what makes it win; Use now only hurried that along.
The dialog's Retry button now also shows while connected, as Reconnect: it tries
the addresses again in their current order and promises nothing about which
answers first. The test results go back to plain rows.
The review also found a test started earlier could overwrite a newer one still
running, and mark it done. Each headset's tests now have a generation; only the
newest one publishes.
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>
The fix review found Use now could still point the wrong way: the button was
worked out from per-address ranks in the devices list while the test was still
running (a better-ranked address might yet answer), and the test's successes
changed those ranks before the list was reloaded.
The test now finishes by publishing the order a reconnect on this network would
try, worked out after it has recorded where each address works, together with
the network it ran on. The page offers Use now only once the test is done, only
for that network, and only on the first address in that order that passed. The
ranks in the devices list are gone again. docs/devices.md no longer promises
where a reconnect lands: a slow address loses to a later one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fix review found two older rules (#titleForm select:focus, .disp select:focus)
that outranked the new select:focus-visible outline and still removed it.
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>
From the second independent review of this PR:
- Use now sends a plain reconnect, which picks the first-ranked address that
answers, but it was offered on every tested address listed above the one in
use. The devices list now carries each address's rank on the current network
(frame_devices.order_addresses), and only the address a reconnect would pick
gets the button.
- Saving one address, cancelling, then editing another: the first save's answer
closed the second editor and lost what was typed. Each edit now has its own
session, and a late answer leaves a newer one alone.
- Switching headsets with the dialog open drew the address offer from the
previous headset's status before it was cleared, so Add could save its IP to
the new headset. The dialog now renders after the old status is cleared.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the independent review of this PR:
- Without a battery reading the chip was hidden, and with it the only way to
the headset's details. It now stays, labelled Headset details.
- On a 375 px phone the menu lined up with the chip and ran 82 px off the
left edge. It now stays 12 px inside the window.
- A long headset name pushed the menu wider; it's now cut short with an
ellipsis (the full name is in the tooltip).
- Reduced motion turned off smooth scrolling for the page but not for the
new scroll area.
- Selects lost their focus ring; keyboard focus now shows a blue outline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An interrupted review pass pointed at four problems in the pill's dialog, each
confirmed against the Frame:
- The offer to add the Frame's LAN address appended it after the Tailscale name,
which then kept winning on that network, so nothing changed. The offer now adds
it first in the list (address-add takes first: true); away from home the
Tailscale name still leads. A tested address that ranks above the one in use
gets a Use now button that reconnects through it.
- A half-typed edit was thrown away when the connection state changed after
focus left the input, and when the server refused the save. The edit row now
stays until it is saved or cancelled.
- Pressing Enter twice sent the update twice.
- The offer's button could act on the previously selected headset.
Keyboard focus also stays on the same button of the same address when the rows
are rebuilt.
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>
At 1260 to 1400 px wide the header was a few pixels too tight and the pill read
"Connected · Tails…"; on a phone it read "Connected · T…". The status word now
always shows whole: when the route doesn't fit beside it, the route drops out
instead of being clipped. The wordmark gives way a little earlier so the route
shows at every desktop width above 960 px, and the smallest phones lose the
logo rather than squeeze the pill.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mac in the headset's picture quality moves into the panel with a label, and the
panel switcher's Open in headset button sits under its list, so neither heading
wraps in a narrow column. Headings line up across panels whether or not they
hold a button. An odd last figure in a stats grid spans the row instead of
leaving a hole. The battery menu says which temperature is the battery's.
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>
The pill says plainly whether the headset is connected and how
("Connected · Tailscale", or the network's name), and never shows a
truncated address. Its dialog now leads with that, then lists the headset's
addresses with what each answered; they can be added, edited, reordered and
removed right there, and a new one is tested straight away. When the Frame
reports a LAN IP on this computer's network that isn't saved, it offers to
add it, so at home the app connects directly rather than over Tailscale.
The connection steps and this computer's network fold away while it's
connected and open when it isn't. Retry now and Set Up Connection only show
when they'd help; the buttons stay in reach when the dialog scrolls.
On the Devices tab the SSH alias, user, port and Forget identity move under
Advanced, and the address kind is worked out from the address. Report a
problem is a speech bubble rather than a warning sign.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The page now scrolls between the header and the Activity bar on a computer,
so the bar never covers content, and nothing overflows sideways from the
760 x 560 minimum up. The header gives way as the window narrows: the
wordmark goes, the tabs tighten, and below 960 px the logo, Refresh (R still
works) and the pill's second line go. Phones keep scrolling the window.
The battery shows once, in the header chip. Clicking it opens a menu with
the headset's storage, memory, temperature, Wi-Fi, uptime, SteamOS build and
services; the big battery card is gone from Home, and the VR performance
table no longer repeats battery and temperature.
Heading rows wrap their buttons onto a new line instead of squeezing the
heading, and every drop-down is dark.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review (GPT-6 Astra, P2): the still-image loop stopped calling show() once
the screen took its first frame, so a surround refused during standby was
never retried and stayed missing for PNG and splat playback until restart.
hold() now keeps draining pending uploads after the screen is shown, until
both are up.
Also: stills and the surround wait out standby without counting as dropped
video frames or tripping the five-minute limit (video only); teardown
errors no longer overwrite a finished status; Stop is ignored once the
outcome is decided.
Tests: PNG and splat where the screen is accepted before the surround
recovers (fail on the old loop); fake-clock coverage of the five-minute
limit and its reset; status keeps filename/metadata layout sources and
explicit layouts stay explicit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Astra review: a switch landing between the check and the assignment could still
install the old headset's tunnel.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integration review findings: the scripts they run ssh'd to whatever 'frame' means
in ~/.ssh/config. They now take FRAME_ALIAS and FRAME_SSH_OPTS from the server's route.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Preserve the complete store, artwork, telemetry, input, media and agent route table alongside the newly landed VR utilities and performance HUD.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Keep the union of server routes, desktop resources and responsive controls. Preserve OpenXR install defaults and telemetry hooks alongside library artwork. Adapt the resource test to single-file entries and avoid a completed-refresh race in the F-Droid test.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Also from review: a SteamVR build without the timing exports can't break status
(AttributeError), and the device test class runs when the file is run directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Real-Frame testing (2026-09-29) found an unworn headset enters standby
within seconds; SetOverlayRaw then returns RequestFailed (23) and the
movie died. The player now drops frames during standby, keeps audio
and pacing, re-sends stills and the theatre surround after waking, and
only errors after five minutes without an accepted frame.
A Stop arriving while the player is already shutting down is ignored,
so a finished video stays 'ended' instead of 'error: Stopped'. The
status now reports the layout's real source (filename/metadata).
Docs record the end-to-end device matrix (API, web UI, CLI).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>