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>
Review round 12: a failed USB-C tunnel now retries the normal path; an
existing HostKeyAlias wins; --host with --usb is rejected; the Steam
desktop-streaming claim is now 'untested' (Valve documents the desktop
showing when a game loses focus); the Show/cleanup overlap test blocks
for real (it fails without the lock). Verified live: with the cable out,
the route is the normal path.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Switching headsets is serialized with the start of any install; background
work counts as running from before its thread starts. use() reroutes every
command at once and makes ensure() wait for the new headset.
- A new user or port reroutes commands even if the attempt then fails.
- ~/.ssh/config edits take a lock file shared with Set Up Connection
(frame_connect.py and connect.sh, which now also writes atomically).
- A finished attempt no longer writes its older settings over a change Set
Up Connection made meanwhile.
- Pin edits are locked and swapped atomically.
- A bare alias behind ProxyJump/ProxyCommand is left to ssh to reach.
- The page drops answers about the previous headset after a switch; the
header switcher takes clicks in the macOS title bar.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Plugged into the Mac, the Frame is a USB network device ("Steam Frame",
usb0 at ~0.9 ms). The tunnel uses it when the Frame's usb0 answers,
with the usual host key; otherwise the normal path. Interleaved runs:
content p50 7 vs 10 ms, click to drawn 17 vs 27 ms, scroll p95 23 vs
31-37 ms. FRAME_MACVIEW_USB=0 turns it off; the card says "over USB-C".
- Bench: --usb, and the route is recorded per run.
- Docs: Steam's own streaming (no SteamVR host on macOS; Remote Play pairs
but streams games, not windows; test blocked); the Frame's USB network;
the 2026-09-28 health-check boot-loop recurrence.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review round 10: a Show replacing its own stream could have its new viewer
ended by the cleanup; a viewer reset left the delayed relay pending.
Verified: the relay delivers what's queued, then closes the agent side.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Chromium on the Frame outlived its last viewer window (verified: 11
processes left after a run). Once nothing is shown, Stop ends it, unless
Show was pressed again meanwhile; its profile is Frame Control's own.
- Relay --delay: if the agent side fails, close the viewer side too
(review round 9).
- Docs: the final scroll run captured 57 fps; don't blame ScreenCaptureKit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in #4, #5, #8 (fbl100's verified Remmina/VNC mirror), APK
alternatives and the website. docs/streaming.md: Mac in the headset stays
the recommendation (now verified on the Frame); Remmina keeps #8's verified
evidence as the whole-screen fallback. docs/mac-in-headset.md cites #8's
lag finding.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review round 8 (GPT-6 Astra xhigh): --delay paused upstream reads, so busy
streams saw 30-60 ms instead of 30. Verified locally: 60-62 ms round trip
with 30 ms each way under load. No saved result used --delay.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Per-frame timing on the Mac's clock (capture, encode, network, decode,
draw), viewer clock sync and reports, input echo, /stats and a HUD.
- scripts/macview-bench.py: repeatable runs on the real Frame, a shaping
relay (no sudo), interleaved A/B between agent settings; results in
bench/results/.
- Adaptive controller: ack-based send gate with jitter-aware slack, AIMD
bitrate that knows when a stream is app-limited, fps then size tiers.
On a 50->3->50 Mbit/s step, scroll p95 went from 4.7 s to 72 ms; no cost
on a clean link.
- Separate mode: real AppKit event loop (HiDPI and NSScreen now work),
cropped capture for fixed-size windows, windows kept on their display,
graceful quit restores windows; stop/start races fixed.
- Encoder timeline clamp (no oversized frame after a pause).
- Frame Control shows each live stream's fps, delay, bitrate and tier.
Reviewed by GPT-6 Astra xhigh (read-only), 7 rounds; findings fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Flatpak installs record their outcome inside main's background job; failed
jobs are diagnostics too. The Privacy panel lives on the Tools page (#privacy
opens it), tab analytics use the four page names, and "Test it now?" reads the
install job's result.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Anonymous PostHog analytics (ui/frame_telemetry.py): usage on by default
after a first-run notice; compatibility results and error details opt-in,
offered together by the notice's "Share more to help fix problems" button.
Random id, no person profiles or GeoIP, scrubbed text, an offline outbox,
and "Show what's been sent" in the new Privacy panel. Inert without a
project key, from a source checkout, or with DO_NOT_TRACK=1.
- APK installs now record install_failed when the APK itself won't install,
and offer a 20-second test after installing. Opted-in reports reach the
shared database through PostHog and `frame_compat_db.py sync`.
- The desktop app updates itself from published releases (app/updater.js):
update.json from releases/latest/download, SHA-256 checked, no downgrades;
macOS bundle swap, Windows NSIS, Linux AppImage, otherwise the release page.
scripts/publish-release.sh publishes a tested draft with its manifest.
- Report a problem (header button, Privacy panel, Help menu) files a GitHub
issue through the website's feedback API, with a previewed, scrubbed
diagnostics snapshot; activity and logs only when asked for.
Reviewed by GPT-6 Astra (xhigh, read-only) three times; all findings fixed.
Docs: docs/privacy.md, docs/releasing.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Steam's own idle timer (60 min on AC, 15 on battery) suspends the Frame,
and SSH work doesn't count as activity. scripts/keep-awake.sh on sets both
timers to Never through Steam's DevTools (reusing ui/frame_steam.py) and
holds a logind sleep inhibitor as a user unit; off releases the inhibitor
and restores the saved timers. Findings recorded in how-the-frame-works.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
macOS leaves the pointer out of the Screen Sharing framebuffer, and neither Remmina showcursor setting brings it back. A Hammerspoon ring around the pointer is a real window, so it gets mirrored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without scale-to-fit a Retina Mac shows 1:1 as a zoomed-in corner. macOS offers Apple auth ahead of plain VNC auth, so Remmina asks for the Mac account login.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menci/archlinuxarm base failed `pacman -Syu` on GitHub's arm64 runner.
Valve's Holo Core aarch64 preview is the base the Frame's SteamOS is built on,
the iPhone app's frame-container already uses it, and its repos carry every
package the fake needs. A failed image build now reruns with the full log.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tests/fakeframe: a container that stands in for the Frame (Arch Linux, or
Arch Linux ARM on arm64) with sshd, rsync, Valve's steamos-devkit-service
and hooks (vendored unmodified), a fake Steam client for the devkit pipe and
the DevTools port (the app's JavaScript runs in Node against stand-in
SteamClient/appStore objects), stubs for steam, wpctl, flatpak, podman,
Lepton and friends, battery and thermal files under /sys, and fault
switches (fakeframe-ctl): pairing mode, approve/deny/timeout, Steam not
running, headset asleep, sshd off, disk full, runtimes missing. A second
container is the computer running Frame Control.
tests/e2e: 29 tests driving the real ui/server.py, frame_connect.py and
frame_titles.py against it; skipped unless FRAME_E2E=1. scripts/e2e.sh
builds, runs and tears down; CI runs it on ubuntu-24.04-arm.
tests/smoke + scripts/frame-smoke.sh: the core cases against a real Frame,
recorded with its BUILD_ID, cleaning up after itself; --pair to pair a
throwaway key. docs/testing.md describes the layers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The build script, launcher and Steam shortcut helper now live in
saphid/chromium-webxr-steam-frame, so drop the duplicate copies here.
The doc keeps the findings, verification and upstream status.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once the SO_PEERCRED patch is applied, gclient sync refuses the modified
checkout, so a re-run failed. Sync once per revision instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tested on a Frame (BUILD_ID 20260922.6101926):
- Devkit pairing: the service runs and answers properties.json, but /register
refuses with "please put the Steam client in pairing mode" unless Steam is on
Settings > Developer > Pair new host. Both setup paths now say so and keep
asking for 2 minutes before falling back to the password.
- Sideloading: registering, launching, quoted start paths and Remove work; a
Windows exe runs under Proton 11 through FEX. An aarch64 build starts but
outside the runtime container, and an x86-64 Linux build doesn't start
because the x86-64 Steam Linux Runtime 4.0 isn't installed. The inspect note
for x86-64 Linux builds now says so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
connect.sh and frame_connect.py now try Valve's steamos-devkit-service
first: GET /properties.json for the login user, then POST /register with
a new RSA key (~/.ssh/id_rsa_frame_devkit, the only type it accepts), so
the user approves a prompt in the headset instead of typing a password.
Port 32000 closed, a timeout or a 403 falls back to the existing
password copy. The Host frame block lists both keys; a host counts as
found if port 22 or 32000 answers; with no host given, dns-sd or
avahi-browse look for _steamos-devkit._tcp. Inferred from Valve's
source, not yet verified on a Frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
chromium-xr.sh steam adds a non-Steam shortcut through Steam's DevTools
port. Chrome's flags move into frame/chromium-xr/launch.sh, which both the
shortcut and launch run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Child processes never inherit the server's stdin. Under the app it's the pipe
held open for --exit-on-eof, and Windows' ssh.exe waited on it forever, so
captures, the screenshot list and Android apps timed out.
- frame_connect's key check accepts a first-seen host key (as the copy step
does), so an already-authorized key doesn't trigger a password prompt.
Verified on Windows 11, Ubuntu and macOS against a Steam Frame: status,
headset and desktop captures, live video, library, Android apps, screenshots
and upload.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Without the maintainer's key, Android compatibility reports stay on the
Mac and the UI says so; the shared database is never contacted.
- Remove personal infrastructure details from scripts and docs: the Drive
folder and gog wrapper now come from the environment, and the Chromium
build host is required instead of defaulted.
- Add an MIT license, tester instructions in the README, and bump to 0.2.0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Flathub Chromium can't enter immersive WebXR on Linux because upstream
only wires the OpenXR device on Windows. Document why, and add scripts to
cross-compile arm64 Chromium with the unmerged Linux OpenXR CLs and to
install, launch and check it on the Frame.
The first build is still running; immersive-vr support is unverified.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Screenshots: list the Frame's Steam screenshots, open them in the
viewer, and save new ones to ~/Pictures/SteamFrame. Ids are validated
before any shell, and copies land atomically.
- Tailscale: scripts/tailscale-on-frame.sh installs a userspace tailscaled
as a lingering systemd --user service with no sudo, SHA-256 checked, safe
to re-run, with --uninstall. docs/tailscale.md covers setup and warns that
in userspace mode every Frame port, including loopback-only DevTools and
ADB, is reachable from the tailnet.
- push-vr-video.sh: filenames starting with "-" are safe, symlinks are
followed, and a real Videos\VR directory triggers a warning.
- Tests cover the screenshot routes (19 total).
Docs keep placeholder addresses for the headset and tailnet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- app: startup shell, python and ssh probes run asynchronously so a slow
shell profile can't freeze the window; PATH comes from the user's real
login shell and a failed lookup isn't cached; a server that never
answers is killed; the setup offer runs once per launch, only after the
UI loads, and decides from HostName alone; connect.sh is started through
`env ... zsh` so it works whatever the login shell is.
- server: volume validates the level before muting or changing anything.
- Steam: null-safe install-manager fields, http.client errors caught in
store ratings, price fallback when a sale has no final price.
- tests: server output kept for diagnosis, any startup error retried, and
captures asserted non-cacheable.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Package the Frame Control web UI as an installable Electron Mac app and
bring in the tooling built alongside it.
- app/: Electron wrapper that starts ui/server.py on a free loopback port,
hardened window (sandbox, no navigation, runAsNode fuse off), login-shell
PATH so Homebrew tools work from Finder, first-run offer to run
connect.sh, ad-hoc signed DMG/zip via electron-builder.
- ui/: headset view (OpenVR screenshots), device status, library, Steam
"Get games" (owned games, install, store search), Android apps as
persistent Lepton instances with a rated F-Droid catalogue and a private
compatibility database, Android display controls over ADB, file and
clipboard transfer, Flatpaks, remote and power actions.
- apk-catalog/, compat-db/, frame/: catalogue build pipeline, Lakebed
capsule for compatibility reports, Frame-side launchers.
- tests/ and CI: server guard and validation tests plus Steam helper tests,
run on Python 3.9 with script and app syntax checks.
- Docs: README leads with the Mac app; new Android, panels, Steam games and
field-notes docs; security notes on LAN-exposed ADB ports.
Screenshot values for the headset's IP and Wi-Fi name are placeholders.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Borrows the nested Plasma session's display and D-Bus variables from
plasmashell and starts the app detached. Tested on the Frame: error paths,
argument quoting, ~ expansion, and `mac-screen` opening a VNC connection.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The headset desktop is nested Plasma in gamescope with no wl-copy/xclip,
so paste-to-frame.sh now calls Klipper over plasmashell's D-Bus bus.
connect.sh, push.sh, paste-to-frame.sh and install-apps.sh were exercised
on SteamOS 0.3.0 (build 20260922); docs record what was confirmed.
Cross-provider review skipped at Alex's request (Astra quota exhausted).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
README with the minimum-typing checklist (Developer Mode toggle + Set User
Password; the rest runs from the Mac), docs for SSH, streaming, file
transfer, and open questions with sourced confidence levels, plus Mac-side
zsh helpers and a fallback headset bootstrap.
Scripts are UNTESTED against hardware: checked with zsh -n / bash -n /
shellcheck only. Two SWE-2 Max read-only review passes (devin -p --model
swe-2-max); verified findings fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>