Installs from the Android store, Import from my Quest and My Meta library
now go into one server-side queue and run one after another, instead of
being refused while another runs. The page shows the queue with Remove
for installs that haven't started; each queued install runs only on the
headset it was queued for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a live test with a real account (71 owned entries):
- the size was cut off behind the package name; sizes now come first;
- a required data file Frame Control can't place as an OBB (The Climb's
1.8 GB main OBB) was never mentioned; the row now says it won't be
installed;
- the count said games, but Meta's list includes apps and environments.
Problems and warnings get their own coloured line; the verdict colours
now win over the row's grey text (they were grey before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The duplicate-name check now compiles each page's scripts together as one
module with node --check, where a top-level name declared twice is a
SyntaxError. Node's parser handles nested scopes, comments and strings,
which the old line scanner did not. Fixture tests show it catches same-line,
cross-block and const/function duplicates and ignores nested ones.
- The live view's loadPanels no longer logs expected offline or stale poll
failures (as refresh() does).
- After a render failure, openStoreApp skips the details request, and
loadOfferDetails no longer writes to a missing #detailInstallNote, which
threw an unhandled TypeError. store_detail.mjs now covers the full open flow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #83: the display search shares one 6 s deadline (2 s per display)
and is remembered (found for good, not found for 30 s), so a stalled display
can't use up the server's 20 s. Control now shows a failed panel listing
instead of "Finding the headset's panels..." when the first one fails.
Page test (tests/page/desk_panels.mjs) covers the Desktop listing: the
headset's reason, initial failure and recovery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking any app in the Android tab opened an empty dialog, which shows as
a single horizontal line (its 1px border). The store's renderDetail was
replaced by the Devices page's renderDetail, declared further down the same
script: a later function declaration silently wins. The store's call then
rendered "Pick a headset." into the Devices panel and left #detailBody empty.
Every platform and both 0.4.x releases were affected.
The store's renderer is now renderAppDetail, and a render failure shows a
clear message with a Close button rather than an empty dialog. The same
clash hid the live view's loadPanels behind the Tools panel switcher's; that
one is now loadPanelSwitcher. Both names match the open store PR (#56).
Tests: a guard that no top-level name is declared twice in the page's
shared script scope, and tests/page/store_detail.mjs, which runs the real
store functions for a good listing and a broken one.
Co-Authored-By: Claude Opus 5.5 <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>
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>