- iPhone build fails if the bundle can't be made, instead of shipping a
stale archive with an empty version.
- A different build that another device is using right now is left running
and replaced once nobody is, rather than stopped under them.
- If what's installed changed after the server looked, the agent asks for
the packages (need-packages) and the server copies them and starts again.
- The installed-build check sends bytes, so Windows' CRLF can't break it.
- Starting again while a stopped start is still copying launches anew;
concurrent copies use their own temporary names.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The keyboard and trackpad no longer fetch KDE Connect from Valve's package
repository on the Frame. The desktop apps and the iPhone app's Frame bundle
carry Valve's arm64 build of kdeconnect 24.02.2-1 and the five libraries it
links (kcontacts, kpeople, modemmanager-qt, pulseaudio-qt, libfakekey),
pinned by SHA-256 in frame/kdeconnect/packages.json and downloaded at build
time from the kdeconnect-frame-24.02.2-1 release, which also holds Valve's
complete source package for each.
On first use the computer copies them over its SSH connection (the iPhone
bundle already has them on the Frame); the agent checks each SHA-256,
unpacks them and stamps which build it is, so later starts copy nothing.
No internet on the Frame, 3.6 MB instead of 8 MB, 18 MB unpacked instead of
82 MB (ModemManager and friends were packaging-only dependencies).
GPL/LGPL compliance: frame/kdeconnect/NOTICE.md names each exact version,
licence and source; per-project licence texts in frame/kdeconnect/LICENSES;
THIRD_PARTY_NOTICES.md; an About and licences dialog in the app.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Home → Keyboard and trackpad, in every version of Frame Control (Mac,
Windows, Linux, iPhone, iPad) with nothing to install on the device in
your hand. On a phone: a trackpad (drag, tap, two-finger scroll and
right-click) and a field that types on the Frame. On a computer: click
the pad to pass the mouse and keyboard through; Esc to stop.
First-party route: ui/frame_input_agent.py runs on the Frame and talks
KDE Connect's LAN protocol (v7) to kdeconnectd as if it were a phone.
KDE Connect isn't on the image, but Valve's package repository has it;
the agent fetches it and four libraries into ~ (no root, survives
updates), starts it, pairs by itself (accepting over D-Bus), and stops
it again when the last device disconnects. Each device has its own
identity; a stuck KDE Connect is restarted once.
Verified against the real Frame (SteamOS 0.4.1): first-time install,
pairing, pointer moves from the Mac's server and the iPhone app
(Simulator), Mac and iPhone at once, two installs at once, a frozen
daemon replaced, and the daemon stopping when the app quits.
Reviewed by GPT-6 Astra (xhigh) over seven rounds; all findings fixed
except per-event delivery acknowledgement (documented known limit).
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>
The share sheet only offers Save Image when the app declares
NSPhotoLibraryAddUsageDescription; without it screenshots couldn't be saved to
Photos. docs/iphone.md now lists what was verified against the Frame (Bonjour
discovery, waiting and reconnecting, upload, share sheet, install links,
opening Steam Link) and the two permission prompts iOS shows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The app finds the Frame by itself (Valve's _steamos-devkit._tcp Bonjour
service, else the saved address or frame.local on port 22), so setup is two
numbered steps: wake your Frame (Looking… / Found ✓, and after a few seconds
exactly what to check), then the Developer Mode password and Connect. Manual
address and key options sit under Other ways to connect.
A paired Frame that doesn't answer is almost always asleep: instead of an
error, Waiting for your Frame says how to wake it and connects as soon as its
SSH port answers (checked every 3 s; 4 s after the stand-in came back).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Valve publishes no Steam Frame OS image, so tests/frame-container builds the
Frame's SSH surface on Valve and Collabora's Holo Core aarch64 base: a
steamos user with a password and sudo, OpenSSH with keys and passwords,
Python, and a systemctl that only records requests.
Against it from the Simulator: password pairing, the host-key pin, the power
password check. Found and fixed: a changed host key or a refused login said
"Can't reach the Frame" and retried forever; they now say "Pair with the
Frame again" and offer that. The server's key rejection now says the header
may be wrong, not only missing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds debug-only test hooks (open on a tab, run page JS, leave the tunnel URL in
Caches) used to drive the app in the Simulator, and records what was verified:
all four tabs with live data, capture and 31 fps live video in WKWebView, upload,
install jobs and the power password check through the app's tunnel.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Pairing saves nothing if it was cancelled while adding the key.
- The ssh stand-in runs under bash: dash refuses job control without a
terminal, which cost stdin and the process group.
- A server that stops while the tunnel opens fails the attempt (and retries)
instead of leaving it half-connected.
- The health probe answers within its deadline even when a dead link keeps
the probe itself waiting.
- A cached version is deleted only if no server runs from it (servers now run
by absolute path) and it's two weeks unused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Cancelling (Change headset, Forget) invalidates the attempt in flight; a
connection only becomes the app's once every step finished for it.
- A server that stops while the tunnel opens is noticed (exit callbacks are
synchronised and replayed), and the app isn't left showing a dead page.
- The port is read only once the digits are complete, and range-checked.
- Other versions in ~/.cache/frame-control stay unless untouched for 14 days,
so a second phone or iPad isn't cut off.
- The key is appended on its own line even if authorized_keys lacks a final
newline.
- The app checks every 20 s, and on returning to the foreground, that the SSH
session still answers, and reconnects if not.
- The ssh stand-in runs the command as its own process group and passes a
TERM on to all of it, so a live-video ffmpeg stops with its stream.
- Touch screens show the library's Play buttons (the rule now follows the
base one); the failure screen only says it's retrying when it is; the page
says the app reconnects rather than naming a desktop menu.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An iPhone can't run Python or ssh, but the Frame can. The app (ios/, SwiftUI)
connects with its own SSH key (Citadel), copies the server and helpers to
~/.cache/frame-control/<version> on the Frame once per version, starts
ui/server.py there with FRAME_LOCAL=1 on the Frame's 127.0.0.1, and shows the
page through an SSH tunnel. The server exits when the phone disconnects.
Server: FRAME_LOCAL=1 puts ui/local-bin on PATH, whose ssh stand-in runs each
`ssh frame COMMAND` locally (and serves as rsync's transport), so desktop and
phone share one code path. Android display goes through podman exec there, as
the Frame has no adb. FRAME_UI_KEY replaces the fixed X-Frame-UI value with a
per-session key. Power actions take the Developer Mode password via sudo -S.
--port 0 now prints the port it took.
Page: a bottom tab bar and safe areas on phones, Play buttons visible on touch
screens, saving through the share sheet, SSH/SFTP/Steam Link/remote desktop
opening in their iOS apps, and a password dialog for power.
App: pairing with the Developer Mode password once (never stored) or with a
key the user adds; host key pinned on first use; plain-language connection
errors with quiet retries; frame-control://install links; alerts and confirms.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>