Skip to content

Connect screen: tiles with each saved Mac's last picture, and groups - #117

Merged
Saffsanity merged 35 commits into
Saffsanity:mainfrom
addhumal:connection-list
Oct 27, 2026
Merged

Saffsanity merged 35 commits into
Saffsanity:mainfrom
addhumal:connection-list

Conversation

@addhumal

@addhumal addhumal commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #116.

What it does

Adds a list/tiles toggle and user-defined groups to the connect screen (ContentView.swift, ConnectionList.swift at pbxproj AF01/FF01), with each saved Mac's tile showing a thumbnail of its last stream. Device only: no wire change, nothing sent to the Mac.

  • List or tiles: a button beside "Connect to a Mac" switches between rows (list, the default) and tiles (Sill.connectionStyle). Tiles use 2 columns from 340 pt wide, and 1 column on narrower screens or at accessibility text sizes.
  • Last stream picture: when a session to a saved Mac ends while Sill is in the foreground, HEVCDisplayView.snapshot(maxWidth: 320, includePointer: false) (shared with Settings panel: Capture Screen #96) saves a 320 pt JPEG to Caches/SillPreviews/<Mac ID>.jpg. It is removed by the context menu's Remove Preview Picture, by Forget, or by storeSavedMacs() when a saved record is retired.
  • Groups: a saved Mac's context menu offers Home, Work, Personal, any custom group names in use, New Group…, and None (Sill.macGroups, cleaned and capped at 32 characters). Grouped Macs appear under their group header in first-seen order followed by Other; when no Mac has a group, ConnectionList.sections returns a single unnamed section so the connect screen is unchanged.

Why

Once you pair with more than one or two Macs (home, office, a headless mini, remote entries alongside local rows), a flat list of identical monitor icons makes them harder to tell apart at a glance. Showing each saved Mac's last stream picture on a tile and letting you group rows mirrors Windows App's Connection Center while keeping existing installs on the flat list until switched.

site/privacy.html notes that preview pictures and group names stay on the device and are removed when you remove the preview, forget the Mac or delete Sill.

Checks

  • Tests/checks/connection-list: 24 checks covering style persistence, group name cleaning and 32-character cap, section ordering, path-traversal rejection in preview filenames, stale file pruning and column count. 16 of 16 mutants caught. Added to CI's mutants list and the checks README.
  • Tests/checks/run-all.sh and swift build -c release pass with only the known warnings; the iOS app builds for the simulator.
  • Verified with harness photos (-SillConnectCase tiles) across Duo, iPad and iPhone sizes at default and accessibility text sizes, and live on an iPad Air simulator against SillHost --synthetic --remote (SillPreviews/<Mac ID>.jpg written on disconnect and shown on the tile on relaunch).

The connect screen shows its rows as a list (as before, the default) or as tiles,
switched by a button beside the title (Sill.connectionStyle). A saved Mac's tile
shows the last picture of its stream, a 320 pt JPEG kept in
Caches/SillPreviews/<Mac ID>.jpg when a session ends while Sill is in front,
removed with Remove Preview Picture, Forget, or once the record is gone. A row's
context menu puts a saved Mac in a group (Home, Work, Personal or a custom name,
Sill.macGroups); with no group set the list is exactly as before. ConnectionList
holds the rules (Tests/checks/connection-list: 24 checks, 16 mutants, in CI's
matrix). No wire change. The privacy policy names the picture and the groups.
…d hint for rows and tiles

The Group menu listed only None and the presets, so no custom name could be made: New Group…
asks for one (cleaned and capped by setGroup). The row and the tile shared copies of their
VoiceOver label, hint and the asking highlight; now one helper each.
# Conflicts:
#	.github/workflows/ci.yml
#	iOSClient/MockCatalog.swift
#	iOSClient/StreamClient.swift
@Saffsanity

Copy link
Copy Markdown
Owner

From a pass over all the open pull requests (nothing is decided yet): #96 adds a nearly identical drawHierarchy snapshot right after videoRect in HEVCDisplayView.swift, so one helper with options should serve both; #99 extends storeSavedMacs() and site/privacy.html at the same spots, so whichever lands second keeps both. #107's Metal layer would sit in front of what previewImage captures, and #66 (with #72 and #73) compiles StreamClient.swift and StreamClient+Remote.swift for its Mac client, where the new UIImage and UIApplication calls need its platform shims; the rest (#93, #94, #95) are the ci.yml line, the checks README and project-file entries.

addhumal added a commit to addhumal/sill that referenced this pull request Oct 15, 2026
@addhumal

addhumal commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. Pushed a700cca (and merged main in a2e1aed): replaced previewImage() with snapshot(maxWidth: CGFloat? = nil, includePointer: Bool = true) -> UIImage?, identical to #96 (29d4910), and called displayView.snapshot(maxWidth: 320, includePointer: false) in tearDown, so #96 and #117 merge without a conflict in either order. Also added New Group… in the Group menu (f388829).

Saffsanity and others added 6 commits October 20, 2026 00:45
Main gained Saffsanity#153 (the checks' build.sh scripts quote their paths) and Saffsanity#142
(SavedMacs: a typed destination survives a key change). Nothing here
conflicted; merged before the polish so it builds on today's main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…locked

Noah on issue Saffsanity#116: tiles are "a toggle and not default behavior". The picture of
the Mac's screen, the privacy-relevant part, was still taken at every session's
end with the list, the default (measured on a private simulator against a
synthetic host: a 640×402 JPEG in Caches/SillPreviews after a list session).

- ConnectionList.takesPreview(style:sceneActive:saved:), pure: only with tiles,
  only from the session's own window in front, only for a saved Mac. The check
  takes every combination (32 checks), and five mutants drop or invert each
  condition (21 of 21 caught).
- tearDown asks it with the display view's window scene, not the app: one of two
  Sill windows can be behind while the app is active, and a window behind draws
  nothing (a black picture over a good one).
- With the list, loadSavedPreviews deletes SillPreviews and loads nothing; the
  switch beside the title and a saved Mac's Show as List delete every picture
  (ConnectScreen's onChange of the style, removeAllPreviews), and a window whose
  client still holds pictures after a switch elsewhere drops them when its
  connect screen appears. Remove Preview Picture shows only with tiles.
- The JPEG is written with complete file protection (Caches, never backed up):
  Sill reads it only at launch and writes it only from a window in front, so
  both happen while the device is unlocked.
- DEBUG console lines: "tiles: kept the picture of …", "no picture … kept: …",
  "the list: every picture deleted".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The helper shared with Saffsanity#96 (HEVCDisplayView.snapshot) gave a picture whatever the
layer showed, and its pointer hide did not reach the screen in time. On a private
simulator against a synthetic host whose scripted pointer moves (the Mac's arrow
over the stream), the stored includePointer: false picture carried the arrow;
flushing the hide's transaction first still left it in 1 picture of 3; holding
the sprite back and drawing after the screen update left it in none of 8, while a
picture with the pointer taken in the same tearDown had it in all 8.

- Nil while no keyframe has gone onto the layer since it lost, or may have lost,
  its picture (showsPicture: apply's flush for a new format, a failed layer's
  flush in enqueue, clear), while the layer is failed or needs a flush to decode
  again (iOS takes the decoder from an app in the background), and when
  drawHierarchy says the drawing is incomplete (a window not on screen).
- Without the pointer: the sprite is held back (pointerHeldBack, which
  placeCursor respects, so a layout pass during the drawing cannot show it) and
  drawHierarchy waits for the screen update (afterScreenUpdates: true only then;
  Capture Screen's picture with the pointer is drawn as before).
- The size is whole points rounded down: rounded to the nearest, a picture took
  in a hairline of the letterbox's black (the last row of every 640×402 JPEG,
  now 640×400).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The tile's picture was a scaledToFill image in a ZStack that only a height
  constrained, so a picture wider than its cell widened the tile: the
  pre-review's render had 163.5 pt tiles in 156.5 pt cells on a 375 pt phone with
  a 16:9 Desktop (3 pt between tiles), and 2.39:1 tiles overlapping. Now the
  92 pt band sets the size and the picture fills it as an overlay, clipped.
- The switch beside "Connect to a Mac" was 36×36 pt: 44×44 now, its symbol where
  it was (centred 22 pt from the column's edge), still an overlay of the title's
  line, so nothing below it moves.
- DEBUG: -SillConnectCase tiles takes -SillVideoSize WxH for the pictures' shape
  (320 pt wide, as a session's end keeps them), so a photo can show a 16:9 or
  2.39:1 picture in its tile; the mock's thumbnails were 208×132 alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…em's choice

The pre-review's nits on the groups, each in ConnectionList (pure) with checks
and mutants (46 checks, 33 of 33 mutants caught):

- New Group…'s Add is disabled for a name that names no group: blank (Add used to
  take the Mac out of its group) or Other in any case, which cleanGroup reserves
  (a group called Other was a second section of that title; a stored one from an
  older build is dropped, so its Macs join Other).
- A preset's name in any case is the preset ("work" is Work), and a name already
  in use in another case joins that group, so one name never makes two sections;
  a name cut at the 32-character cap no longer ends in a space.
- Sections come in a stable order, the presets in theirs, then other names as
  Finder sorts them, then Other, whatever order the Macs are listed in (first
  seen moved sections as Macs came online); the Group menu lists them the same.
- The Group menu is a Picker (inline in its submenu): the system's checkmark on
  the current group and VoiceOver's "selected", for the trailing checkmark images.
- A record retired for a newer one of the same Mac (SavedMacs.superseding, the
  re-pairing that replaces a record whose Mac has a new key) gives its group to
  the newer one, unless that one has its own (ConnectionList.carryingGroup), before
  the list is stored, which drops the groups of records no longer saved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The PR's paragraph sat under the pairing paragraph, said the pictures were "for the
connection tiles" while the list took them too, had no release markers and left
the page's date. Now one item of "What stays on your devices › On your iPhone or
iPad": only while you show your Macs as tiles, a small picture of each paired
Mac's screen as your last session with it ended; it stays on the device, can't be
read while the device is locked, isn't in backups, and is deleted when you switch
back to the list, remove it, forget the Mac or delete Sill; group names stay on
the device too. It sits between `<!-- Tiles` and `<!-- /Tiles -->`, and part 1 §3
of the release checklist gains the perl line that takes it out of a republish
before the release that ships it (tried on a copy of site/: with both perl lines
privacy.html is main's but for the date, which the checklist puts back). Last
updated October 20, 2026. The privacy manifest needs nothing: no new
required-reason API, nothing leaves the device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Saffsanity and others added 20 commits October 20, 2026 09:31
Main gained Saffsanity#110 (Shift+Return and Shift+Tab), Saffsanity#136 (apps in folders), Saffsanity#87 (the
virtual display's BOOL encoding, the virtual-display-api check) and Saffsanity#88 (a universal
Sill.app on request). Two conflicts, each keeping both sides: ci.yml's mutants matrix
(connection-list and virtual-display-api, sorted) and the checks README's sentence on
where each check came from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/ci.yml
#	Tests/checks/README.md
addhumal merged main (4d990e9) into the branch at 89dc48c himself, the same
merge as e7a52e1. The two differ only in where Tests/checks/README.md wraps the
connection-list sentence; this keeps e7a52e1's wrap, so the tree is e7a52e1's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four files met, each kept whole on both sides: ci.yml's mutants matrix and the
checks README's history name both connection-list and display-wake; CLAUDE.md's
list of checks has both; the release checklist marks the display wake's words
and the tiles' (Display wake and Tiles markers) and keeps both perl lines. With
all three cuts applied to a copy of site/, the clipboard grep prints only
privacy.html's log line and the display wake's and the tiles' greps print
nothing, as the checklist says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto connection-list

Five files met; each keeps both sides:
- ci.yml's mutants matrix names connection-list and keyframe-recovery, sorted.
- DEVELOPMENT.md's harness list: the tiles bullet and the -SillHUD note, then Saffsanity#93's
  -SillLayerFail bullet.
- The release checklist's paragraph on marked words names all four markers (Clipboard,
  Keyframe, Display wake, Tiles) once; main's copy repeated the Clipboard and Display wake
  sentences after the Keyframe ones. With the four perl lines on a copy of site/, the
  clipboard grep prints only privacy.html's log line and the keyframe, display wake and
  tiles greps print nothing.
- The project file: ConnectionList (AF01/FF01) beside KeyframeRecovery (AC11/FC11).
- HEVCDisplayView.swift: Saffsanity#93 replaced enqueue's failed-layer branch with checkLayer, which
  flushes an interrupted picture from a frame, the layer's notifications and the window
  becoming active. The snapshot's showsPicture now goes false in checkLayer after its flush
  and true after the keyframe that ends a wait goes onto the layer (git put that line in the
  DEBUG breakLayer, where `resuming` is not in scope). Live on a private simulator with
  -SillLayerFail 6 against a synthetic host that ignores keyframe requests: a session that
  ends while the picture waits keeps no picture, and the one kept before stays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main brings Saffsanity#159, Reverse Scrolling (issue Saffsanity#143): ScrollDirection in
StreamProtocol, the device's switch in the Settings panel's This iPad group,
a finger's drag over the picture and the portrait trackpad's two-finger scroll
turned around with their coast, InputScript's drag and padscroll steps, its
words in privacy.html and support.html between Reverse Scrolling markers, and
protocol's ScrollDirection cases and mutants.

Where it met Saffsanity#117 (the connect screen's tiles and groups):
- Tests/checks/README.md: the closing sentence names both, `connection-list`
  with Saffsanity#117 (Saffsanity#116) and `protocol`'s Reverse Scrolling cases and mutants with
  Saffsanity#159 (issue Saffsanity#143). The connection-list row (Saffsanity#117), and the protocol row and
  the lede's Reverse Scrolling phrase (Saffsanity#159), merged by themselves.
- docs/release-checklist.md: both sides had rewritten the paragraph on marked
  words the same way and each added its own marker. It now names every marker
  once, Clipboard, Keyframe, Display wake, Tiles and Reverse Scrolling, with
  every grep, and both perl lines stay (Tiles, then Reverse Scrolling). Run on
  a copy of the merged site: the five perl lines leave no marker, the
  clipboard grep prints only privacy.html's log line and the other four print
  nothing, also with either feature's words left unmarked.
- Merged by themselves, each checked against both parents: CLAUDE.md (Layout's
  ScrollDirection, InputOverlay and TrackpadView lines and Build and run's
  lines beside Saffsanity#117's ConnectionList and check list; neither side has a
  Current step entry), docs/DEVELOPMENT.md, site/privacy.html (each item
  inside its own marker), iOSClient/ContentView.swift (Saffsanity#159's harness contract
  lines and InputScript's steps beside Saffsanity#117's connect screen and its contract
  lines) and iOSClient/StreamClient.swift (reverseScrollKey beside macGroups,
  macPreviews and the tile's picture at tearDown). No Swift file conflicted.
  ci.yml's mutants matrix is Saffsanity#117's (Saffsanity#159 left it alone): sorted, with
  connection-list and protocol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Send Audio as a beta (Saffsanity#94, the audio branch: kind 43, the host's
capture, encoder and packetizer, the device's decoder, playout and output, the
Send Audio row, the audio-codec, audio-packetizer and audio-playout checks and
ledger's mutants). Where it met the connect screen's tiles and groups (Saffsanity#117):

- .github/workflows/ci.yml: the mutants matrix is the sorted union,
  connection-list beside main's audio-codec, audio-packetizer, audio-playout
  and ledger (52 checks).
- CLAUDE.md: Layout's iOSClient paragraph keeps the tiles' sentence after the
  tour's line, then main's AudioDecoder, AudioPlayout, AudioOutput and
  StreamClient+Audio lines; the checks list keeps main's re-wrapped audio and
  away names, then connection-list before device-gate. Main's Current step
  entry for the sound and its Build and run lines merged by themselves (this
  branch has no Current step entry).
- Tests/checks/README.md: main's compatibility row (the sound's JSON, 140
  cases and 34 mutants), then the connection-list row; the history keeps
  "connection-list with Saffsanity#117 (Saffsanity#116)" in its first sentence, then main's audio
  sentences whole. Main's lede phrase for the sound merged by itself.
- docs/release-checklist.md: the marked-words paragraph is main's wording (a
  republish before those releases deletes the blocks of each release not out
  yet) naming Clipboard, Keyframe, Display wake, Sound (PR Saffsanity#94), Tiles (PR
  Saffsanity#117) and Reverse Scrolling once each, with both greps ('sound|send audio',
  'tiles|group name'); the perl lines keep Sound's and Tiles', six in all. On
  a copy of the merged site/, the six lines leave no marker and the greps
  print what the paragraph says.

Merged by themselves, each differing from the other parent by exactly that
side's hunks: iOSClient/ContentView.swift (main's scene phase for the sound
and its harness lines, the tiles' connect screen and harness lines),
MockCatalog.swift (main's sound settings cases, the tiles connect case),
StreamClient.swift (main's sound in init, the moves, the reader and tearDown;
the tiles' groups, previews and tearDown's picture, taken before the sound
stops), the project file (AA01-AA04 and FA01-FA04 beside AF01/FF01, each
defined once, plutil OK), docs/DEVELOPMENT.md, and site/privacy.html (the
Sound items inside their markers, the Tiles item inside its own).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…napshot of the screen

On Noah's iPad mini the Desktop streamed fine and the tile's picture, taken
at the session's end, was black. HEVCDisplayView.snapshot drew the view with
drawHierarchy(in:afterScreenUpdates:), which on a device does not capture an
AVSampleBufferDisplayLayer's video: it is composited outside the app's own
drawing. The simulator composites it in software, so every simulator photo of
a tile looked right.

The picture now comes from the video itself. StreamClient keeps the session's
last keyframe with the parameter sets it came with (`lastKeyframe`, on the
network queue: the payload as received, one frame, replaced at each keyframe,
dropped at tearDown). At the session's end, on the same conditions as before
(connected, the video size known, a saved Mac, tiles, the session's own
window foreground active: ConnectionList.takesPreview), KeyframePicture
decodes it with a VTDecompressionSession of its own made from the parameter
sets' CMVideoFormatDescription (hardware on a device, software in the
simulator; the display layer's decoder is never touched), asks VideoToolbox
for BGRA at the tile's size (ConnectionList.previewPixels: 640 pixels wide at
most, the video's shape, the height rounded down; once more at the frame's
own size should that fail), draws it into an sRGB bitmap and gives the
picture, off the main thread on a queue of its own, given up after 1 s
(ConnectionList.decodeBound). It lands on main: kept only while tiles are
still on and the Mac still saved (ConnectionList.keepsDecodedPicture), then
savePreview shows it on the tile at once (`macPreviews` is published, so a
connect screen already showing picks it up) and writes the same JPEG as
before (quality 0.72, Library/Caches/SillPreviews/<macID>.jpg, complete file
protection). A decode that fails or gives no answer in time, or a picture
black throughout (ConnectionList.looksBlack: no colour channel brighter than
24 of 255), writes nothing and the old file stays, with a DEBUG `tiles:` line
saying why.

The drawHierarchy path is gone with what existed only for it: snapshot,
showsPicture, pointerHeldBack and placeCursor's guard, the afterScreenUpdates
dance; HEVCDisplayView.swift is main's again, checkLayer and the keyframe
recovery untouched. The Mac's cursor is never in the video and the device's
pointer is a layer of its own, so no keyframe carries an arrow, and the
-SillHUD readout is no longer in the picture (ContentView's harness contract
said it was).

Tests/checks/connection-list: 70 checks (the picture's size at every shape,
the black test with padded rows, the fourth byte, the boundary at 24 and the
bad layouts, when a decoded picture lands), and 15 new mutants among 48.
pbxproj AF02/FF02 for KeyframePicture.swift (no open pull request uses them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is taken

The keyframe kept can be from long before the session's end: the encoder
makes one at a new source and every 4 x fps frames it encodes, so none while
a window stays still, and the changes since are in later frames only. When
the session's end is Sill's own (Disconnect, the only one a connected session
has: another Mac is picked from the connect screen) and its end keeps a
picture, Disconnect now asks the Mac for a keyframe (kind 44, Saffsanity#93's request:
a still window's last frame encoded again) and ends the session when it
comes or after 300 ms (ConnectionList.freshKeyframeWait), whichever is first;
the stream screen stays meanwhile and a second tap does nothing. The wait is
told of the keyframe on the network queue (`keyframeWanted`, set there before
the ask goes out); a session that ends otherwise during it ends as the
Disconnect did, with no reconnect. A session lost or ended by the Mac decodes
the keyframe kept.

ConnectionList.freshPicture (pure): only when the end keeps a picture
(takesPreview), something streams (the host asks its encoder for nothing
then) and the Mac takes the request; within the picture's recovery's spacing
of its last ask (KeyframeRecovery.spacing, 2 s, so the Mac's one a second
never refuses one) no other ask: wait for that ask's keyframe while it is
due, else the keyframe kept is the one it brought. Which Macs take kind 44
(ConnectionList.takesKeyframeRequests): nothing on the wire says so, and
Saffsanity#93's recovery sends it to every Mac because an older one skips it; but a
skipped ask here would hold Disconnect for the whole 300 ms, and the request
must never go to a Mac that does not take it. So only a Mac whose window list
says a version after 0.5.1, the last Sill for Mac without it, is asked. Main's
Sill.app builds take it and still say 0.5.1 until the next release, and
SillHost says no version: they are not asked, and their picture is the
keyframe kept. DEBUG `-SillFreshKeyframe 1` asks any Mac (the live tests'
SillHost).

Tests/checks/connection-list: 218 checks (the rule over every combination,
the spacing's boundary at 2 s, eighteen versions), compiled with
KeyframeRecovery.swift and StreamProtocol's Compatibility.swift; 64 of 64
mutants (16 new).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5bb5

4bb5bb5 is the merge of main at 74d099c (Saffsanity#94, Send Audio as a beta) into
the pull request's head 46684c1, with its resolutions (ci.yml's matrix,
CLAUDE.md, the checks README, DEVELOPMENT.md, the release checklist, the
project file, privacy.html and support.html). Merged here into the two
commits on 46684c1 that take the tile's picture from the stream's last
keyframe. One conflict: StreamClient.handle's frame case keeps both, this
branch's keyframe store (and Disconnect's wait for it) and main's
frameForSound before the display layer's frame. The tree is 4bb5bb5's plus
exactly those two commits' changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DEVELOPMENT.md's tiles paragraph says how the picture is taken now (never a
snapshot of the screen, which a device draws black where the video is; the
session's last keyframe decoded as it ends, off the main thread, a second at
most; Disconnect's fresh keyframe first, up to 300 ms, only from a Mac that
says a version after 0.5.1; a failed or black decode keeps the old picture),
and its harness list drops "-SillHUD is in the picture" for
-SillFreshKeyframe 1. CLAUDE.md's Layout entry names ConnectionList's new
rules, the keyframe store and KeyframePicture (pbxproj AF02/FF02). The checks
README's row: what connection-list compiles and checks now, 218 checks and 64
mutants. privacy.html gains, between its own Tiles markers, the request for
the picture once more as you disconnect, in the list of what a device sends
its Mac; the release checklist's markers paragraph names it (the perl line
and the grep already cover every Tiles block; tried on a copy: every grep
prints what it should).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's Disconnect has no finger in the simulator gates, and with tiles on
it now does more than end the session: it asks the Mac for a fresh keyframe
first, waits up to 300 ms, then the tile's picture is decoded. `disconnect`
calls it as the panel's button does, t seconds after the session's first
window list, under the script's guard (a session dialled to a loopback
address whose host sends no version: never Sill.app). ContentView's harness
contract, DEVELOPMENT.md and CLAUDE.md's Debug harness list say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found live on the private simulator: with the picture interrupted
(-SillLayerFail) and the Mac ignoring keyframe requests, the recovery's own
repeat ask, due 4 s after its last, went out during Disconnect's 300 ms wait,
within the Mac's second of Disconnect's ask, and the Mac refused it ("Keyframe
requests ignored: 1 in a minute, from devices asking more than once a
second"). When Disconnect asks, the recovery's pending look is cancelled: the
session ends within the wait, and KeyframeRecovery keeps asks 2 s apart so
the Mac never refuses one. KeyframeRecovery and HEVCDisplayView's checkLayer
are as main has them. Run again: two asks reached the Mac (the recovery's,
then Disconnect's), none refused, and the stored keyframe's picture was kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noah, trying the build on his iPad: switched from tiles to the list and back,
the thumbnail must come back. Switching to the list (the switch beside the
title, a saved Mac's menu) and a launch with the list deleted every picture.
Now a style never deletes one: ConnectionList.previewPlan (pure) says what a
launch or a change of style does. With tiles every saved Mac with a file shows
its picture, read from the file; with the list none shows and every file
stays, so tiles show them again at once. Either way the files of Macs no
longer saved go, as the pruning at a launch did. Only Remove Preview Picture
and Forget delete a Mac's own. StreamClient.syncPreviews(to:) applies the plan
at launch and from ConnectScreen's onChange and onAppear (another Sill
window's change), only when the style differs from the one the shown
pictures follow, so a connect screen that comes back keeps a picture that
landed meanwhile (and the harness's mock keeps its own). removeAllPreviews
and loadSavedPreviews are gone.

A session that ends while the list shows still takes no picture
(takesPreview unchanged) and leaves the kept one as it was: the list is the
default, and a Mac's screen is kept on the device only by someone who chose
tiles; the privacy page's promise ("Only while you show your Mac computers as
tiles") stays true. Taking it in the list style too would be one condition in
takesPreview and a sentence on the privacy page.

Tests/checks/connection-list: 228 checks (the round trip tiles, list, tiles;
a launch with each style; a removed picture, a forgotten Mac, names that are
not a saved Mac's own .jpg), 73 of 73 mutants (nine new, among them the
switch to the list deleting the files and the pictures not shown after
switching back). DEVELOPMENT.md's tiles paragraph, CLAUDE.md's Layout, the
harness contract, the checks README, and privacy.html's Tiles item (the list
hides the pictures and keeps them until removed, the Mac forgotten or Sill
deleted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noah, on his iPad: "for devices in the tile list, if it's the same device, I
want the tile shown on all windows (i.e. both wired, wireless, direct and
remote) as all tiles show the same device, so should show the same preview."
His Mac showed as Wired and Remote, and only one of the two tiles had the
picture. A tile looked its picture up by FoundMac.macID, which a network row
taken for the saved Mac by its Bonjour name alone (no tag of a saved Mac's:
DiscoveryPolicy.rowMac, FoundMac.savedByName) does not have; that same lack
keeps the Mac's Remote row listed beside it.

ConnectionList.tileMac (pure): the saved Mac a row stands for is the one its
tag names (a network or Direct row) or its record (a Remote row), else, for a
row with no tag, the one it is taken for by name, as a tap dials it. Every
row of one Mac (Wired, Wi-Fi, Direct, Remote) looks its picture up by that
Mac's ID and shows the same file; another Mac's rows show their own; a row of
no saved Mac shows the placeholder. The groups' sections use the same rule,
so the row taken by name is listed under its Mac's group beside its Remote
row rather than under Other (a one-line change to undo if Noah prefers). Its
context menu stays as it was (none: it may be a stranger's until its key
says otherwise).

The harness's -SillConnectCase tiles shows the Mac mini twice, its Wired row
taken by its name and its Remote row, with one picture. Tests/checks/
connection-list: 235 checks (one Mac as Wired, Wi-Fi by its name and Remote
sharing one picture; two Macs each their own; an unsaved row; a row by its
name alone; the tag before a name; every row of one Mac under its group), 76
of 76 mutants. DEVELOPMENT.md, CLAUDE.md's Layout, the harness contract and
the checks README say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained the pairing window shown to screen sharing (Saffsanity#157, issue Saffsanity#108:
Settings › Devices' Show the pairing window to screen sharing,
PairingWindowSharing and the pairing-sharing check, README, SECURITY.md,
DEVELOPMENT.md and the plans, support.html's item marked <!-- Pairing window).
Where it met the connect screen's tiles and groups (Saffsanity#117):

- .github/workflows/ci.yml: the mutants matrix is the sorted union,
  connection-list and pairing-sharing both in (53 checks, exactly the
  folders with a mutants.py).
- Tests/checks/README.md: both rows (connection-list after compatibility,
  pairing-sharing after pairing-address); the history's first sentence names
  connection-list with Saffsanity#117 (Saffsanity#116), then Reverse Scrolling's with Saffsanity#159 and
  pairing-sharing with Saffsanity#157 (Saffsanity#108), and main's re-wrapped audio sentences
  follow unchanged. Main's lede phrase merged by itself (this branch has
  none).
- docs/release-checklist.md: the marked-words paragraph names Clipboard,
  Keyframe, Display wake, Sound, Tiles (PR Saffsanity#117), Reverse Scrolling and
  Pairing window (PR Saffsanity#157) once each, with every grep ('tiles|group name'
  and 'screen sharing' among them), one to a line; the perl lines merged by
  themselves, Tiles' and Pairing window's among them, seven in all. On a
  copy of the merged site/, the seven lines leave no marker and the greps
  print what the paragraph says.

Merged by themselves, each differing from the other parent by exactly that
side's hunks: CLAUDE.md (main's Current step entry for Saffsanity#157 at the top, this
branch having none; Layout's PairingWindowSharing beside the tiles'
paragraph; the checks list with connection-list and pairing-sharing; Build
and run's defaults list with showPairingWindowToScreenSharing beside the
tiles' -SillSettingsScript disconnect step) and docs/DEVELOPMENT.md (main's
Through Screen Sharing item and Settings › Devices sentence, the tiles'
paragraphs and harness lines). site/privacy.html (the Tiles items) and
site/support.html (the Pairing window item) changed on one side each,
inside their own markers; the project file on this branch's side only
(plutil OK, each id once).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Chinese input and dictation (Saffsanity#147, issue Saffsanity#138: the stream's
input view a UITextInput, TextComposition and TextInputRules with the
text-composition check, StreamClient's onCaretMayMove and sendAfterInput,
which StreamScreen wires to the input view, the touch rig compiling
TextComposition.swift, README, DEVELOPMENT.md's Other languages and
dictation, the hardware keyboard plan, support.html's paragraph marked
<!-- Other languages, and the next What's New's paragraph). Where it met the
connect screen's tiles and groups (Saffsanity#117):

- .github/workflows/ci.yml: the mutants matrix is the sorted union,
  connection-list and text-composition both in (54 checks, exactly the
  folders with a mutants.py).
- docs/release-checklist.md: the marked-words paragraph names Clipboard,
  Keyframe, Display wake, Other languages (PR Saffsanity#147), Sound, Tiles (PR Saffsanity#117),
  Reverse Scrolling and Pairing window once each, in that order, with every
  grep ('dictation|Control-Space' and 'tiles|group name' among them), one to
  a line as this branch has them; the perl lines merged by themselves, Other
  languages' and Tiles' among them, eight in all. On a copy of the merged
  site/, the eight lines leave no marker and the greps print what the
  paragraph says.

Merged by themselves, each differing from the other parent by exactly that
side's hunks: CLAUDE.md (main's Current step entry for Saffsanity#147 at the top,
this branch having none; Layout's UITextInput and TextComposition beside the
tiles' paragraph; the checks list with connection-list and
text-composition; Build and run's -SillSettingsScript disconnect step as
this branch has it), Tests/checks/README.md (both rows, main's lede phrase,
both history clauses), docs/DEVELOPMENT.md (main's Other languages and
dictation section and the checks' sentence, the tiles' paragraphs and
harness lines), the project file (TextComposition's AC0138/FC0138 beside
ConnectionList's AF01/FF01 and KeyframePicture's AF02/FF02; plutil OK, each
id once) and iOSClient/StreamClient.swift (main's onCaretMayMove before a
pick, a window's button, a launch, a gesture, a menu item and a click or
key, with sendAfterInput, beside this branch's Disconnect that waits for a
fresh keyframe, the keyframe kept and tearDown's picture: separate
functions). site/privacy.html (the Tiles items), site/support.html (the
Other languages paragraph) and docs/app-store-metadata.md (the What's New
paragraph; the draft 1,571 characters, as main says) changed on one side
each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noah's decision, from his iPad: his Mac says 0.5.1, so the version rule never
asked it, and the tile kept the session's first keyframe and missed the
window he had brought forward since. Disconnect now asks every Mac, as Saffsanity#93's
picture recovery does: "Any Mac from main onwards answers within a frame; an
App Store 0.5.1 Mac ignores it and the only cost is a 300 ms pause before the
connect screen appears." The 300 ms wait stays, and so does cancelling the
recovery's pending ask while Disconnect waits.

ConnectionList.freshPicture loses its Mac's-version input (ask only when the
end keeps a picture and something streams; the recovery's 2 s spacing as
before), and takesKeyframeRequests, its 0.5.1 constant and the SillVersion
import go; so does DEBUG -SillFreshKeyframe 1, which only forced the ask (no
other use). Tests/checks/connection-list compiles without Compatibility.swift:
154 checks (the freshness grid over every combination, every Mac asked), 72
of 72 mutants (the version mutants gone; "no Mac asked" new). DEVELOPMENT.md's
tiles paragraph and harness list (SILL_TEST_KEYFRAME_IGNORE stands in for a
Mac that skips the request), CLAUDE.md's Layout, the harness contract, the
checks README, and privacy.html's Tiles item ("a request for one more
picture").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…02db6

0502db6 is the merge of main at 48a4ef3 (Saffsanity#147, Chinese input and
dictation: TextComposition, the text-composition check) into the pull
request's head 4630dd3, with its resolutions. Merged here into the commit
on 4630dd3 that has Disconnect ask every Mac for the fresh keyframe; no
conflict, and the tree is 0502db6's plus exactly that commit's changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained capture at the display's own pixel scale (Saffsanity#233: Sources/SillHost,
the capture-scale check, its README row, CLAUDE.md's new Current step entry).
One conflict: ci.yml's mutants matrix, now the sorted union (connection-list
beside main's capture-scale, 55 checks). CLAUDE.md, the checks README and
DEVELOPMENT.md merged by themselves, both sides kept (main's entry first in
Current step, both rows in the README). No Swift file met.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Saffsanity
Saffsanity merged commit 3c25748 into Saffsanity:main Oct 27, 2026
2 checks passed
Saffsanity added a commit that referenced this pull request Oct 27, 2026
Main brings #117 (the connect screen's tiles and groups, addhumal) and
#234 (the capture scale follow-ups). Only iOSClient/ContentView.swift
conflicted, in SettingsScript's doc comment: both sides kept (this
branch's `set ... sendAudio` and #117's `disconnect` step). CLAUDE.md,
Tests/checks/README.md (#117's connection-list row, #234's capture-scale
count and this branch's rows), docs/DEVELOPMENT.md, MockCatalog.swift
(#117's tiles connect case beside this branch's settings cases),
StreamClient.swift and site/privacy.html (#117's Tiles items beside the
Sound item; the date October 27, 2026) merged by themselves. CLAUDE.md's
Current step gains this branch's entry, first.

Verified on the merge: swift build -c release; the iOS app, Debug for
the generic simulator as CI builds it, only the known StreamClient
capture warning; run-all.sh ledger away-quality phone-portrait tour
connection-list capture-scale, all passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
No conflicts: Saffsanity#117's connection list (ContentView, MockCatalog, StreamClient, a new check) and this
branch's key row above the keyboard (DuoPosture, PortraitStreamScreen, InputOverlay) meet only in
Tests/checks/README.md's table, which merged by itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 27, 2026
Noah, 2026-10-27: "The list/tiles switch should always show", whichever style
is on and whether or not a Mac is listed. #117 showed it for tiles alone, and
this branch's first rule only once a saved Mac was listed; both left someone
with no saved Mac a way to lose it. Only the card, which takes the title's
place, and a link's confirmation leave it out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 27, 2026
PR #117 (the connect screen's tiles and groups). No conflict: CLAUDE.md's Layout and Build and run,
Tests/checks/README.md's rows (connection-list beside this branch's), docs/DEVELOPMENT.md and
StreamClient merged by themselves; nothing of #117 reads the browsers' results or the TXT helpers
this branch replaced, and none of the files the four checks of DiscoveryPolicy.swift compile changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 27, 2026
PR #234 (capture scale follow-ups, host only) after #117. CLAUDE.md conflicted, both adding an entry at
the top of Current step: both kept, this one first. Tests/checks/README.md and docs/DEVELOPMENT.md merged
by themselves; none of the files the four checks of DiscoveryPolicy.swift compile changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 27, 2026
The defaults Noah set on 2026-10-27, one PR after #117: the connect screen shows tiles unless its person chose the list, and the list/tiles switch always shows; Sill.app's Virtual Display and Send Audio are on by default as registered defaults that a saved key always wins over (the CLI keeps --virtual-display and --audio opt-in); the virtual display is never made with a side under 800 points, which macOS ran at 1x (the five 1x displays in Sill.log, an iPhone held upright). Verified 2026-10-27 (the verdict in the handoff): run-all 55 of 55, 162 of 162 mutants over four checks, the CLI's output identical to main's, the bare app registering both defaults and writing nothing, photos on a 27.1 Duo simulator, then the review's word fixes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
addhumal pushed a commit to addhumal/sill that referenced this pull request Oct 27, 2026
Noah, 2026-10-27: "make tiles not list the default behavior of the app.
... Of course, if a user changes a setting remember their choice for
future sessions as you do now." ConnectionList.defaultStyle is .tiles
and storedStyle reads a stored Sill.connectionStyle, "list" or "tiles",
as it is; nothing stored, or a value no build writes, reads as the
default. ConnectScreen keeps the key optional (@AppStorage String?), so
the rule alone decides, as StreamClient's connectionStyle already did;
the switch and a saved Mac's menu write it through toggleStyle, as
before. Only an install that never toggled changes.

With tiles the default, a picture of each saved Mac's screen is kept as
its sessions end unless the list is chosen (takesPreview is unchanged:
only with tiles), and Disconnect asks for the fresh keyframe first.

The switch beside the title now shows once a saved Mac is listed,
whichever style is on. Saffsanity#117 also showed it whenever tiles were on, its
way back from tiles while the list was the default; with tiles the
default that showed it on a fresh install and took it away under the
finger of someone with no saved Mac who chose the list, with no way back
until they paired one. The list's rule is Saffsanity#117's as it was.

DEBUG: -SillStyleToggleAt <s>[,<s>...] toggles the style as the switch
does, for the gates that have no finger; the console's connect: lines
say the style and what is stored.

Tests/checks/connection-list: 162 checks (154 before), 76 of 76 mutants.

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.

Feature Request: Connection list as tiles with each Mac's last picture, and groups

2 participants