Skip to content

Stream: follow a window the streamed app opens in front, and come back - #182

Open
grub-basket wants to merge 2 commits into
Saffsanity:mainfrom
grub-basket:feat/follow-new-windows
Open

grub-basket wants to merge 2 commits into
Saffsanity:mainfrom
grub-basket:feat/follow-new-windows

Conversation

@grub-basket

Copy link
Copy Markdown
Contributor

Opening a new window of the app being streamed (iTerm's New Window, an app's "Check for Updates…" dialog, a Settings window) leaves the device on the old window. In regular mode only the picked window is captured, so the window the app put in front never shows; today the stream moves to a new window only after a launch from the device (pendingLaunch).

This makes Sill for Mac follow it, the way the Mac's own screen would, and come back when it closes. Mac only: no wire change, nothing on the device.

  • WindowFollow (pure, new): the frontmost window on the Mac (topmostWindow(at: nil)) is followed when it is new (never listed before since the first device came, so not a window back from the Dock, a hidden app or another Space; never from the first catalog), listed now, and of the same app as the window streaming, by bundle ID (an empty bundle ID never matches; the streamed window's app comes from the previous catalog when it closed in the same poll). back() unwinds a stack of windows followed from, skipping any that closed, so a dialog over a dialog over the main window comes back one at a time.
  • StreamCoordinator.windowsChanged: after a pending launch, and only in regular mode (on the virtual display the app's other windows already show over the picked one) and not mid-switch, a followed window is selected without bringForward (it is in front already), with one line: "Following Claude's new window 1234 (from window 987)." When the followed window closes and the stack has a window still listed, the stream goes back to it (brought forward) with a line, instead of stopping. A device's pick or a launch clears the stack.
  • On by default; defaults write me.saffer.sill.mac followNewWindows -bool NO turns it off. No Settings row yet: happy to add one (or flip the default) if you'd rather.
  • Tests/checks/window-follow: 20 checks, 11 of 11 mutants caught; in the checks README and CI's mutants matrix.

Known limits (your call; happy to change any of them):

  • Following goes through the normal pick, so with a window fitted to the device (Feature Request: apps revert back to their original Mac shape when disconnecting #57) the window followed from goes back to its own size while the new one is fitted, and is fitted again on the way back.
  • If macOS lists a sheet (Save, an alert) as a window of its own, the sheet streams without the window around it. I haven't checked which sheets ScreenCaptureKit lists.
  • A follow whose switch fails (a window gone before it is picked) ends the stream rather than going back.
  • A window that sits in the catalog for a poll before it comes to the front is not followed.

Tested: swift build -c release (only the CaptureProbe warning); Tests/checks/run-all.sh, all passing, on this branch (cut from main at 4d990e9); the new check and its mutants; an independent review, whose findings are fixed in the second commit or listed above. Not tested live: it needs real windows and Screen Recording, so it hasn't been run against a real app stream yet. If it misbehaves in practice I'll update this PR.

Related to #111 (new windows go first in the bar), but independent of it.

🤖 Generated with Claude Code

grub-basket and others added 2 commits October 20, 2026 08:36
Reported on a device: opening a new window of the app being streamed
(iTerm's New Window, Claude's "Check for Updates..." dialog) left the
iPad on the old window. In regular mode only the picked window is
captured, so a window the app put in front of it never showed; the
stream switched to a new window only after a launch from the device
(pendingLaunch).

- WindowFollow (pure): the frontmost window on the Mac
  (topmostWindow(at: nil)) is followed when it is new since the last
  catalog (never from the first one), listed now, and of the same app
  as the window streaming, by bundle ID (an empty bundle ID never
  matches; the streamed window's app is taken from the previous
  catalog when it closed in the same poll). back() unwinds a stack of
  windows followed from, skipping any that closed, so a dialog over a
  dialog over the main window comes back one at a time.
- StreamCoordinator.windowsChanged: after a pending launch, and only
  in regular mode (on the virtual display the app's other windows
  already show over the picked one) and not mid-switch, a followed
  window is selected without bringForward (it is in front already),
  with one line ("Following Claude's new window 1234 (from window
  987)."). When the streamed window closes and the stack has a
  window still listed, the stream goes back to it (brought forward),
  with a line, instead of stopping. A device's pick or a launch clears
  the stack.
- On by default; `defaults write me.saffer.sill.mac followNewWindows
  -bool NO` turns it off (no Settings UI yet).

Known limits: a window that is in the catalog for a poll before it
comes to the front is not followed (the next poll no longer counts it
as new); with an Aa scale set, a followed dialog is fitted to the panel
like any pick.

Tests/checks/window-follow: 18 checks, 9 of 9 mutants caught; README
and CI's mutants matrix.

Tested: swift build -c release, the check and its mutants. Not tested
live: it needs real windows and Screen Recording, and the Mac's Sill
was in use from an iPad at the time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From an independent review of the previous commit. The catalog lists only
on-screen windows, so "not in the last catalog" also matched a window that
came back rather than one the app opened: un-minimized from the Dock, its
app shown again after being hidden, or in front after a switch to another
Space. The stream jumped to it, and switching Spaces back and forth bounced
between two windows while the follow stack grew.

- WindowFollow.next takes `seen`, every window ID a catalog has listed
  since the first device came; a window in it is not new. The last catalog
  (`previous`) still judges the first poll and the streamed window's app.
  This is the same rule the owner suggested for new windows' place in the
  bar on Saffsanity#111 (new only the first time it is listed in a session).
- StreamCoordinator keeps `seenWindows` beside `lastCatalog`, and both, with
  the follow stack, start afresh when the first device comes and when the
  last one leaves. Before, a catalog taken with no device (the CLI started
  with a preselected window) was compared with the first poll after a device
  connected, possibly hours later, and could follow a window at once.

Left as known limits, for the owner's call (in the PR): following goes
through the normal pick, so with a window fitted to the device the window
followed from goes back to its own size (Saffsanity#57) and the dialog is fitted,
then the first window is fitted again on the way back; whether sheets are
listed as windows of their own (they would then stream without the window
around them); and a follow whose switch fails ends the stream rather than
staying on or returning to the first window.

Tests/checks/window-follow: 20 checks (2 new: a window back from the Dock or
another Space is not followed; one never listed is), 11 of 11 mutants
caught (2 new, 1 pattern updated). Tested: the check and its mutants,
swift build -c release, and Tests/checks/run-all.sh on the previous commit.
Not tested live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant