- A set-up headset whose alias goes through a jump host (ProxyJump or
ProxyCommand in ~/.ssh/config) is reached through it, address by address,
still pinned per headset.
- A refused switch puts the header's switcher back on the headset in use.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A command that fails after a switch doesn't make the connector drop the new
headset's connection.
- On first import, the app keeps using the `frame` headset even when Set Up
Connection put another block above it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Terminals, power and a reconnect's probes use the route commands have now
(a pinned bare-alias destination, a login change still deferred).
- Saving port 22 keeps an explicit Port line where there was one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A bare alias's route is pinned to where ~/.ssh/config sent it when it was
routed (HostName, Port, User), so editing that file can't move an install.
- Renaming during an install is allowed: only a real user or port change waits.
- The Devices tab follows a network change even while the headset is offline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Retry now (while connected) and Forget identity wait for running installs.
- Terminal windows get the headset's address by name, so a link-local IPv6
zone never has to pass through Windows' console.
- Renaming the headset in use shows at once in the header.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Every command to a set-up headset checks its pinned key
(StrictHostKeyChecking=yes, whatever ~/.ssh/config says); only the
connector's first handshake may save one.
- A reconnect during an install keeps the whole route it started with, also
when a bare alias is set up meanwhile.
- Removing the headset FRAME_ALIAS named doesn't bring it back as a bare alias.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A reconnect while an install runs keeps the login it started with; a new
one from ~/.ssh/config applies after.
- Frame > Open SSH goes through the server, so it uses the same headset and
address as the app and refuses when there's none.
- A bare frame alias in use when a headset is set up stays selectable.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A headset set up while a bare alias is in use doesn't take over by itself;
a login change from ~/.ssh/config waits for running installs.
- Saving a headset writes only the login fields that changed, and only if the
block still holds the old ones.
- SSH, SFTP, power and remote desktop open with the same headset and address
as every other command, and refuse when there's no address.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Removing or moving the active headset's address waits for running installs,
like switching.
- A volume change still waiting to be sent goes to the headset whose slider
it was, and a switch cancels it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A title waiting its turn to be read stays with the headset it was dropped
on, and is dropped if the app switches meanwhile.
- Probes still finishing from an earlier attempt can't overwrite the rows of
a newer one.
- The FRAME_ALIAS the server started with stays on the list after switching
away, so it can be picked again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A batch of dropped files stays with the headset it was dropped on, and
stops if the app switches.
- Removing or moving the address in use reroutes at once; a headset with no
addresses reaches nothing rather than whatever ~/.ssh/config says.
- A late answer to an older device-list request is ignored.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Removing every headset leaves none in use (commands fail at once) instead of
falling back to the `frame` alias.
- ssh goes to the IP that answered for IPv6 too, with a link-local address's
interface (verified: frame.local over fe80::…%en9 on the real Frame).
- Find results only show in the panel of the headset they were for.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The headset a change is meant for is checked and the work counted in one
step, so a switch can't slip in between (uploads too).
- A sideloaded title read on one headset can't be installed on another; open
confirmations close on a switch.
- The only headset can't be removed while its ssh alias stays behind.
- Answers about the previous headset are dropped without touching panels; the
catalogue's Installed tags are rebuilt for the new headset.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A switch publishes the new headset at once, so the page clears the old one's
panels and the lists behind them (games, store, Android apps, screenshots).
- The page names the headset its changes are for (X-Frame-Device); the server
refuses one meant for a headset it has switched away from (409).
- A rejected address edit changes nothing.
- Test now goes to the IPv4 address that answered, like the connection.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A switch clears every headset-specific list and its buttons at once.
- SSH goes to the IPv4 address that answered the probe, not the name again.
- A rejected headset edit changes nothing.
- The SteamOS/Lepton builds recorded in reports are read again per headset.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Attempts carry a generation: one overtaken by a switch, a removal or a login
change routes nothing back to the old headset and can't report connected.
- Removing the active headset or changing its user/port reroutes at once,
before anything that can fail.
- One known_hosts file per headset (~/.ssh/frame-control-hosts/<id>):
forgetting one headset's key can't drop another's, whoever else writes.
- learn() checks, under the config lock, that the block is still what the
attempt started from before writing to it.
- A switch stops live video and drops captures from the previous headset.
- A probe shares its time between the addresses a name resolves to.
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>
- No switching headsets (or changing the active one's user/port, or removing
it) while installs run: they read the ssh settings step by step.
- Switching reroutes every command to the new headset at once, even if it
never answers.
- ~/.ssh/config edits are serialized, use unique temp files, and back off if
another program wrote the file meanwhile.
- stop() ends a handshake in progress and joins the connector.
- Pinned keys are written unhashed (HashKnownHosts=no); hashed ones are still
found and forgotten via ssh-keygen.
- Set Up Connection changing a headset's user or port updates the registry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Frame Control can now manage more than one Steam Frame, and reach each at any
of several addresses (LAN IPs per network, its .local name, Tailscale). A
connector in the server tries them all at once, picks the best one that
answers, follows ssh -v through each stage (network, finding, SSH, identity,
login) and streams that to the page. The header shows it live; a new Devices
tab (key 5) manages headsets, addresses and network names.
- ui/frame_devices.py: registry in devices.json, imported from the managed
~/.ssh/config blocks; per-headset host key pinning; config block updates.
- ui/frame_network.py: gateway IP+MAC fingerprint, Wi-Fi name, Tailscale.
- ui/frame_link.py: the connector, Test now, Tailscale/mDNS discovery, API.
- server.py: ensure_master delegates to the connector; /api/connection,
/api/connection/events (SSE), /api/devices.
- Electron: headset switcher and Devices item in the Frame menu.
- frame_connect.py --alias; FRAME_CONTROL_DATA_DIR / FRAME_CONTROL_SSH_DIR
keep tests off real data.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main now runs Android installs as background jobs and maps SSH failures to
one offline message. Alternative-version installs go through the same job,
the alternatives dialog waits on it with runJob, and send_error_json keeps
the apk blocker that opens the dialog.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Gradient buttons showed a dark sliver on the right: the background shorthand reset
background-origin, so the gradient repeated under the transparent border.
- Phone header: keep the brand on one line and drop the GitHub button under 480px.
- Hero pill: put the platform list on its own line on phones instead of orphaning one item.
- Privacy page: plain sections instead of open accordions whose x looked like a close button.
- Same header (GitHub button) and footer links on every page; legend spacing on the form.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One malformed or unreachable repo no longer hides the others; skip bad
index entries; a refreshed raw index outdates its reduced copy; style the
dialog like the others; validate package ids with PKG_RE.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Rate limiting fails open on KV errors instead of dropping feedback
- Break owner/repo#1, GH-1 and github.com references in user text
- Time the form with the browser's monotonic clock, not wall-clock
- Serialize lgtm approvals and rebase before pushing
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reducing an index no longer deletes apk-catalog/data/index-v2.json, which
the catalogue build reads. Drop the measurement log from docs.
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>
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>
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>
- Header, content, tab bar, status strip, drawer and toasts add the notch,
Dynamic Island, rounded-corner and home-indicator insets on every side
(zero on desktops). Phones on their side use the bottom tab bar layout.
- The phone tab bar hides while a text field has focus, instead of riding
on the keyboard.
- DEBUG hook FRAME_TEST_LANDSCAPE for checking this in the Simulator.
- docs/streaming.md: iPhone mirroring (UxPlay, broadcast extension) and
keyboard/mouse input (uinput needs no sudo on the Frame, verified).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>