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>
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>
* Live view: a Desktop view that stays still, and Control to tap on the Frame
The live view gets a second source and a way to use the Frame from it:
- Desktop: the app panel in use in the headset, streamed from its own window
(x11grab of gamescope's redirected window), so it doesn't move as the
wearer looks around. A picker shows any other panel, view only.
- Control: on the Desktop view a tap or click lands exactly where you put it;
drag is a mouse drag, press and hold right-clicks, two fingers scroll, and
on a computer the mouse, wheel and keyboard work directly. On the headset
view the view is a trackpad. A text field and key row type from a phone.
Input goes through gamescope's own EIS socket (the way Steam feeds Remote
Play input) with the libei already on the image: ui/frame_touch.py, over
the same long-lived ssh machinery as the keyboard agent, nothing to
install. It reaches the panel that has focus on either X display, which
the KDE Connect route can't. Verified on the Frame and from the iPhone app
in the Simulator.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: fixes from review
- Keys held on the Frame are released with buttons when Control stops or
the view loses focus; keys for the Frame no longer trigger Frame Control's
own shortcuts.
- Taps only act when the picture on screen is the panel in use; positions,
presses, keys, text and scrolls name their panel (display and window: ids
repeat across :0 and :1, told apart by pid), and the Frame drops them if
focus has moved on. Releases always go.
- While connecting, a tap keeps its position; on an error only releases wait
and retries back off; trimming a long queue never drops a release.
- Lifting one of two scrolling fingers ends the scroll; a cancelled touch
isn't a tap; clicks and holds on the bars around the picture do nothing.
- A capture loop from before a Live restart can't stop the new video.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: close the targeting gaps from the second review
- The focused panel's display comes from GAMESCOPE_FOCUS_DISPLAY (gamescope
packs ":1" into the first value), so a window id repeated across :0 and :1
can't be mistaken; the pid is only the fallback.
- Presses, keys, text and scrolls read focus afresh on the Frame; only moves
use a reading up to a second old.
- A gesture remembers the panel it started on and does nothing more if that
stops being the one in use; a press with no panel to aim at isn't sent.
- Opening a screenshot clears the panel Control would act on; switching to
another app releases held keys and buttons.
- Trimming keeps a click with its position; the error backoff holds for new
input too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: fixes from the SWE-2 Max review
- The Frame side tracks keys as well as buttons and lets go of both when the
session ends.
- A stale tap tells the page, which re-reads the panels at once.
- A paused input device waits instead of ending the session; only a
disconnect does. An OS error on one event skips it.
- Presses check focus with two property reads and do the full lookup only
when it changed.
- Writes to an agent's stdin are serialized, so two devices sending at once
can't tear a line (the keyboard agent too).
- Connecting gives up with a message after 15 s instead of hanging on
"Connecting…"; text goes in 100-character pieces so releases don't wait
behind a long paste; a cancelled mouse gesture releases what's held.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: stale clears, pauses reconverge, pastes split per request
- The Frame says it has caught up as soon as an aimed event lands after a
stale one, so the page stops re-reading the panels.
- After a device pause it lets go of everything it holds (releases that
arrived while paused were dropped), and waits for the device once per
batch, not once per event.
- The quick focus check no longer freshens the panel geometry's age.
- Each request carries at most about 100 characters of text.
- Turning Control off while it connects doesn't report an error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* frame_touch: build the socket path on the Frame, so Windows can import it for tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* frame_touch: any event that goes through clears the stale flag
A trackpad move names no panel, so waiting for an aimed event could leave
the page re-reading panels for the rest of the session; a release still
aimed at the old panel doesn't count.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stop always calls systemctl and treats exit 5 (unit already collected)
as done, so there is no is-active/stop race. Cleanup never masks the
copy error, the play ssh timeout covers the remote worst case, and
tests cover stop exit codes and systemd-run stderr reporting.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Stop is a no-op when the collected player unit is already gone
(raw systemctl stop exits 5 on the Frame; verified 2026-09-29).
- Surface systemd-run stderr when the player can't start.
- Keep the copy error if the cleanup ssh also fails; reject upload
names that the play path can never accept.
- Allow 60 s for play (ffprobe 30 s + systemd-run 15 s remote).
- Docs: four-hour cap is unconditional; no delete action yet; fix a
garbled timing sentence.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The live API rejects mimes=image/jpeg on /logos and /icons, so every logo and
icon lookup fell back to generated art. Found with a real key: SuperTux now
gets grid, wide, hero and logo; Beat Saber all five.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Title removal ran the Steam shortcut tidy-up before steamos-delete, which
finds the Proton prefix through that shortcut, so compatdata was left behind
(e2e caught it). steamos-delete runs first again; art/collection tidy-up after.
- Test fixtures are byte-exact: never convert line endings (a text-looking
fixture APK got CRLF on Windows and failed its SHA-256).
- Read index.html/artwork-settings.js as UTF-8 in tests; app-data backup and
OBB shell tests run only on POSIX (they exercise the Frame-side scripts).
- e2e expects the icon under artwork/ now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Final review follow-up. A failed Thread.start() leaked a resolver slot (four
failures disabled artwork lookups). GIF graphic-control blocks must have the
fixed 4-byte payload (otherwise dropped) and an image with no pixel data is
rejected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The handshake runs after the watchdog can reach the TLS socket, with the
remaining time as timeout, and the watchdog shuts the socket with the plain
socket method. At most four lookups that outlived their deadline may run; more
fail at once with a clear error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The screen and first frame must be at most 4096x4096 and the frame inside the
screen; anything malformed or truncated is rejected. Only a minimal
single-frame GIF (header, screen, colour table, graphic control, first
image) reaches the Frame's Chromium, however many frames the source has.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Matching the APK path in command lines could hit an unrelated process, and
the pgid file had a registration race. The launcher keeps the flock (not
inherited by Lepton) and, holding it, stops only lepton-steamlaunch-<instance>,
whose name is this app's alone. A Lepton host process may linger briefly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Automatic backfill touches only entries marked art_pending at install (a
devkit title Steam registered later) and fills only slots Steam has no art
for: no name, exe, VR flag or icon changes, no clearing. Older installs
without the flag are left alone and refreshed only when the user asks.
- Android remove takes the install lock that install and refresh hold, so a
refresh in progress can't recreate a removed app; a refresh after removal
finds it not installed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Name resolution runs in a thread within the budget, a watchdog shuts the
socket at the deadline, and the body is read one receive at a time with the
remaining time as timeout. SteamGridDB goes through the same bounded fetch,
without redirects.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lock isn't inherited by Lepton, so a launcher killed before Lepton made its
container left an untracked Lepton that a new Play could overlap. The launcher
records its child's process group and, once it holds the lock, ends a recorded
group that is still running this app.apk (never an unrelated reused id).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GitHub's opengraph preview is repo text, stats and an identicon; as a Steam hero
it looked broken. GitHub entries no longer default to it (the store draws its
fallback, Steam gets generated art). Source GIFs (common gameplay captures) are
accepted; the Frame's Chromium draws the first frame. Open Saber Plus uses its
gameplay GIF as banner. Verified on the Frame: hero/wide now show gameplay.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 'Refresh artwork' (settings) and the API's refresh-art --all cover devkit
titles as well as Android apps; frame_titles.py gains refresh-art ID|--all.
- Apps and titles without complete Steam artwork are flagged (art_missing):
the app shows 'Add artwork', and the CLIs' list prints the refresh command.
- When the app lists them and Steam answers, Frame Control re-applies their
art in the background (at most every five minutes), e.g. for a title Steam
registered after an install made while it wasn't running.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Remove: collections and artwork clearing are best effort in Steam's JS, and
the host carries on to delete the files when Steam isn't running (Android
apps and devkit titles).
- Devkit titles: the shortcut is found by devkit id, the saved id, or an
executable/start folder inside the title's folder; never by display name.
Steam's overviews don't carry devkit_gameid (checked on the Frame
2026-09-28), so 'list' now reads exe/start dir from app details.
- Photo-based grid/wide/hero slots render as JPEG (a noise-heavy 3840x1240
hero was over 12 MiB as PNG on the Frame); the logo stays transparent PNG.
Rendering gets 75 s and retries once with generated art. Each slot is
cleared before it is set, since Steam keeps .png and .jpg side by side.
- Devkit titles keep their own VR flag (vr=None skips SetShortcutIsVR) and
their Sideloaded collection.
- A failed title install's cleanup can't replace the original error.
- refresh_art for devkit titles (frame_titles.refresh_art).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Any failure of a source image or SteamGridDB (HTTPException, odd JSON) now
becomes a warning and generated art, never an aborted install; refresh-art
--all reports each app and carries on.
- URL artwork goes through apk_sources._images: public addresses only, at most
three redirects, and one overall deadline for all of an install's fetches.
- PNGs are checked from their header only (any depth or interlace; Steam's
Chromium decodes them), JPEGs may have trailing padding, and 4K screenshots
are within limits. The slow pure-Python decoder is gone.
- SteamGridDB title matching keeps letters of every script and never matches
on an empty name. One warning per source slot, not per candidate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once flock is held no launcher owns a running container (a SIGKILLed launcher
left it), so it is stopped and the launch continues. fd 9 is closed for the
Lepton child so it can't keep the lock held.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>