Repository navigation
Connect screen: tiles with each saved Mac's last picture, and groups - #117
Conversation
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
# Conflicts: # .github/workflows/ci.yml
|
From a pass over all the open pull requests (nothing is decided yet): #96 adds a nearly identical |
# Conflicts: # .github/workflows/ci.yml # Tests/checks/README.md
|
Thanks. Pushed a700cca (and merged main in a2e1aed): replaced |
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>
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
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>
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>
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>
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>
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>
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>
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)
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>
Closes #116.
What it does
Adds a list/tiles toggle and user-defined groups to the connect screen (
ContentView.swift,ConnectionList.swiftatpbxprojAF01/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, the default) andtiles(Sill.connectionStyle). Tiles use 2 columns from 340 pt wide, and 1 column on narrower screens or at accessibility text sizes.HEVCDisplayView.snapshot(maxWidth: 320, includePointer: false)(shared with Settings panel: Capture Screen #96) saves a 320 pt JPEG toCaches/SillPreviews/<Mac ID>.jpg. It is removed by the context menu's Remove Preview Picture, by Forget, or bystoreSavedMacs()when a saved record is retired.Home,Work,Personal, any custom group names in use,New Group…, andNone(Sill.macGroups, cleaned and capped at 32 characters). Grouped Macs appear under their group header in first-seen order followed byOther; when no Mac has a group,ConnectionList.sectionsreturns 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.htmlnotes 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.shandswift build -c releasepass with only the known warnings; the iOS app builds for the simulator.-SillConnectCase tiles) across Duo, iPad and iPhone sizes at default and accessibility text sizes, and live on an iPad Air simulator againstSillHost --synthetic --remote(SillPreviews/<Mac ID>.jpgwritten on disconnect and shown on the tile on relaunch).