- 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>
- site/: landing page, /feedback/ and /privacy/ on Cloudflare Pages
(frame-control.pages.dev). POST /api/feedback validates the form and opens
a labelled issue with a fine-grained token; honeypot, minimum fill time and
KV rate limits keep spam out. Ko-fi donate buttons appear once the page
name is set in site/public/js/site.js.
- .github: the issue and PR gate from badlogic/pi-mono. New contributors'
issues and PRs are auto-closed; a maintainer replying lgtmi/lgtm approves
them via APPROVED_CONTRIBUTORS. Issue templates and CONTRIBUTING.md.
- CI runs the website tests; README points feedback at the form.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A thumbnail under the screenshot opens the 66-second trailer, which is attached to the 'trailer' release.
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>
Combines the three feature branches on bundle-deps. Conflicts in server.py,
index.html, preload.js, README and test_server.py keep both sides. The
web-install downloader now uses urllib's default HTTPS context, so the
bundled CA list from 770f26c applies to it on Windows too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Dropping a game's .zip, folder or .exe on Send to Frame now adds it to the
headset's Steam library through Valve's SteamOS Devkit title path, with the
runtime picked from the program's header: Windows PE -> Proton Experimental
(steam_play=1), aarch64 ELF -> SteamLinuxRuntime_4-arm64, x86-64 ELF ->
SteamLinuxRuntime_4 (through FEX). Other architectures are refused.
- frame/devkit-utils: Valve's devkit-utils vendored unmodified (MIT,
steamos-devkit v0.20260925.1), synced to ~/devkit-utils by stamp, bundled in
the app and compiled in CI.
- ui/frame_titles.py: inspect (safe unzip, ELF/PE classification, launch
target ranking), install(path, name=None, exe=None, runtime=None,
progress=None), list, launch, remove, plus a CLI.
- ui/server.py: /api/titles (inspect/install/discard/launch/remove),
/api/titles/job progress, and an upload mode 'title'.
- ui/index.html: confirm dialog (name, launch target, runtime), install
progress, and a Sideloaded titles list with Launch and Remove. The app's
preload passes a dropped folder's path.
- tests and docs/sideloading.md. Device-side behaviour is inferred from
Valve's source; the headset was offline, so none of it has been checked on
a Frame yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A site can link to frame-control://install?manifest=URL (or ?url=URL) to
install a title with Frame Control. Manifests use FrameDrop's format, so
framedrop.install/v1 is accepted as well as frame-control.install/v1.
- app/install-link.js parses links; main.js registers the scheme (plus
electron-builder protocols for Info.plist and the .desktop file), takes
links from open-url, second-instance argv and the first argv, and holds
them until the page asks for them through preload's onInstallLink.
- ui/frame_webinstall.py checks the URLs (HTTPS only; localhost over http
only when the link itself is local; no userinfo; every address public,
rechecked on redirects and pinned for the connection), reads the
manifest, downloads with a size cap and sha256 check, and dispatch()
sends .apk to frame_android and .zip/.exe to frame_titles when present.
- server.py adds /api/webinstall/check, start, job and cancel behind the
existing Host and X-Frame-UI guards; a start needs a one-time id from
check. Downloads stop on cancel and on shutdown, and leftovers from a
killed server are swept by PID.
- index.html asks before anything downloads (name, source host, file,
type, size, whether a sha256 was given) and shows progress.
- docs/web-install.md, docs/install.html (landing page, unpublished).
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>
- app/build/fetch-deps.js downloads a standalone Python 3.12
(python-build-standalone) for every build and adb from Google's
platform-tools, pinned by SHA-256, and prunes what the server never
uses. It replaces the Windows-only embeddable Python.
- The app runs the bundled Python with -I -u -B -X utf8, so a PYTHONHOME
or PYTHONPATH meant for another Python can't break it and nothing is
written inside the signed macOS bundle.
- An adb you already have still goes first, so two adb versions don't
keep restarting each other's server. arm64 Linux has no official
platform-tools and keeps using the system adb.
- ui/frame_apk.py reads APK badging (package, label, version, min SDK,
ABIs, icon) from the binary manifest and resources.arsc, replacing
aapt2. It matches aapt2 on nine F-Droid APKs and finds launcher icons
the old path missed with adaptive icons.
- The app reads the computer's clipboard through Electron, so Linux no
longer needs wl-clipboard or xclip.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Run the server with -X utf8: the bundled Windows Python ignores PYTHON* variables.
- Quote every argument in Windows terminal commands, so cmd metacharacters are literal.
- frame_connect: accept HOST:PORT, validate input, retry the config swap while
Windows' ssh.exe holds ~/.ssh/config locked, and don't apply 0o700 on Windows.
- Never use rsync on Windows; unbounded stream queue; validate FRAME_ALIAS;
more Linux terminals; bundle the window icon; docs and wording fixes.
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>
Live in the headset view now streams video instead of polling stereo
screenshots (~2 fps). SteamVR's steamvr-v4l2cam.service mirrors the headset
view into /dev/video99; ffmpeg on the Frame encodes it with x264 (720p30 by
default, AUD + repeated SPS/PPS), /api/stream relays the raw H.264 over SSH,
and the page splits it on access unit delimiters and decodes it with
WebCodecs into the existing viewer. Capture still takes a stereo still; the
desktop panel keeps capture polling, and the page falls back to it if the
video can't start.
The remote ffmpeg runs under a shell that kills it when the SSH channel
closes, stderr goes to a temp file, and a 10 s stall ends the stream. One
stream at a time; a new one supersedes the last.
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>