Skip to content

Allow pairing QR codes on remote desktops with an opt-in setting - #157

Merged
Saffsanity merged 11 commits into
Saffsanity:mainfrom
scripsweave:fix/remote-desktop-qr-code
Oct 27, 2026
Merged

Saffsanity merged 11 commits into
Saffsanity:mainfrom
scripsweave:fix/remote-desktop-qr-code

Conversation

@scripsweave

@scripsweave scripsweave commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #108.

The pairing window always sets NSWindow.sharingType to .none, so Apple Remote Desktop hides the QR code and typed pairing code. Add Display QR code on remote desktops in Settings → Devices. It is off by default and persists across launches. Enabling it sets the pairing window to .readOnly; changing it updates an existing window immediately, without restarting the host or regenerating its code.

The setting explains that viewers of a shared screen can use the pairing code. Sill's own desktop streams and window catalog continue excluding Sill's windows. Other dialogs keep their existing behavior.

Validation:

  • swift test: both new regression tests pass (default, persistence, existing/new window sharing, and no host reconfiguration). Uses isolated preference suites and never starts the host.
  • swift build -c release: passes; existing CaptureProbe pointer warning only.
  • Scripts/make-app.sh: builds and verifies a locally signed app bundle.
  • Reviewed Devices pane previews in light and dark mode.
  • Tests/checks/run-all.sh: all existing check suites pass.

An Apple Remote Desktop connection has not been tested end to end; the regression test verifies the actual AppKit window's sharing mode.

@Saffsanity

Copy link
Copy Markdown
Owner

From a pass over all the open pull requests (nothing is decided yet): the only overlaps with other open PRs are mechanical: Package.swift's target list, where #66 (and #72 and #73, built on it) adds its Mac client targets, and Sources/SillMenuBar/HostSettings.swift, where #94 adds a key and a registered default after shareClipboard, so whichever lands second keeps both. #108 reports the same hidden pairing window through Screens and Screen Sharing, and #137 is proposed to close as its duplicate, so Fixes #108 here would link the issue that stays open.

Saffsanity and others added 9 commits October 19, 2026 22:11
…check

Issue Saffsanity#108 (and Saffsanity#137): the pairing window's sharingType = .none hides it
from Screen Sharing, Apple Remote Desktop and VNC apps such as Screens, so
a Mac used only through them could pair a device over the USB cable alone.
This keeps the PR's opt-in switch, off by default and applied at once, and
holds it to what Noah asked for: the pairing window only, as a toggle.

- Named plainly: "Show the pairing window to screen sharing" (saved as
  showPairingWindowToScreenSharing), in Settings › Devices after Require
  pairing, with a footnote saying why it is off.
- The rule is PairingWindowSharing (pure): hidden while off, shown
  (.readOnly, AppKit's default) while on, whoever opened the window, the
  Mac's user or a device's ask, since a device's tap is how a Mac seen
  over Screen Sharing pairs. PairDeviceWindowController applies it as the
  window is made, as each offer shows, and at once on a change (the app
  delegate's hook, in place of a closure the controller installed over
  HostSettings). The cable notice and every other window are untouched.
- Tests/checks/pairing-sharing (16 checks, 8 of 8 mutants, in CI's
  mutants matrix) replaces the XCTest target and its `swift test` CI step:
  this repository's checks are pure swiftc ones. AppModel's
  init(defaults:), there only for that test, goes with them.
- Sill.log says when it changes, and at launch while it is on;
  -SillSetAfter takes showPairingWindowToScreenSharing=0|1 (and the hook's
  list now names shareClipboard, which it already took); the previews draw
  the pane with the switch off in every state and on in a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a user and a developer need to know about Settings › Devices' "Show
the pairing window to screen sharing" (issue Saffsanity#108):

- README's Pairing bullet: where it is, that it is off by default, and why.
- docs/DEVELOPMENT.md: Sill.app's Settings › Devices, and a Pairing at home
  bullet on pairing a Mac used through Screen Sharing, Apple Remote Desktop
  or Screens (a Mac without a display): the switch, a tap on the device or
  the menu, turning it off afterwards, the log line, the bare app's hook.
- site/support.html: one item under "If it doesn't work", between
  `<!-- Pairing window` markers, so a republish before the release that
  ships it can cut it; docs/release-checklist.md gains that cut beside the
  clipboard's, with the grep that proves it.
- SECURITY.md: the switch among the risks that are known and by design.
- docs/home-pairing-plan.md: its open risk of a code in a screen share now
  says the switch makes it so, by choice.
- CLAUDE.md: Layout (PairingWindowSharing, the pairing-sharing check), the
  defaults key in Build and run, and a Current step entry with what was
  verified and what is left for a real Screen Sharing session.

The App Store's next What's New is the device's changes alone, and this
change is Sill for Mac's, so it stays out of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#153 (the checks' build.sh quote their paths) and Saffsanity#142
(Saved Macs keep a typed destination across a key change). Merged so
this branch builds and checks against today's main; no file conflicted.

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

PairDeviceWindowController's mapping from PairingWindowSharing's answer
to NSWindow.SharingType (.hidden to .none, .shown to .readOnly) is the
one part of the rule that reaches Screen Sharing, and no check compiled
it: swapping its two cases would have inverted the switch with CI green
(the verifier's finding).

The mapping moves into PairingWindowSharing.swift under
#if canImport(AppKit). Tests/checks/pairing-sharing imports AppKit for
that enum alone (no window, no screen) and checks both values by name,
since the enum prints only its type's name: 18 checks, 2 new. Three new
mutants (the two cases swapped, hidden told .readOnly, shown told
.none): 11 of 11 caught. The checks' README row and CLAUDE.md's Layout
say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The footnote's "(a Mac without a display, say)" and README's "such as
one without a display" came right after "another remote desktop app",
so the example read as an app without a display (the verifier's
finding). Both now say "as you would a Mac without a display", which can
only be the Mac; README keeps its second person ("If you use your
Mac…").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
§6.2, the pairing window's spec, still said the window is
sharingType = .none with nothing else, the one stale statement of it
left in the docs (the verifier's finding; the 2026-10-19 triage named
this line beside docs/home-pairing-plan.md's, which the branch had
updated). It now adds Settings › Devices' "Show the pairing window to
screen sharing": off by default, .readOnly while on, whoever opened the
window, applied at once to one already open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an't

The bullet said that while the switch is on, anyone who sees the Mac's
screen remotely, "a screen recording or a video call's shared screen
included", can read a code, which implied that with it off they cannot.
ScreenCaptureKit is reported to ignore .none from macOS 15 (the code's
own comments say so), so a recording or a call may show the code either
way (the verifier's finding). It now says what the switch changes, what
Screen Sharing, Apple Remote Desktop and VNC viewers see, and that a
screen recording or a call's shared screen may show the code either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#110 (Shift-Return and Shift-Tab), Saffsanity#136 (apps in folders,
the installed-apps check), Saffsanity#87 (the virtual display's BOOL encoding, the
virtual-display-api check) and Saffsanity#88 (a universal Sill.app on request).
Merged so this branch builds and checks against today's main. Only
ci.yml's one-line mutants matrix conflicted: both sides kept, sorted
(46 checks: main's installed-apps and virtual-display-api beside this
branch's pairing-sharing; ledger has no mutants).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d on main

CLAUDE.md's entry for the switch says what the verifier found after
fa57c2f and how each finding was fixed (the window's sharing type in the
pure type and its check, 18 checks and 11 of 11 mutants; the Mac's
example in the footnote and README; the remote access plan's §6.2;
DEVELOPMENT.md on what the switch changes and what it can't), and what
was run again once main was merged in at 4d990e9: a clean release
build, all 47 pure checks, the previews against main's and the window
server's sharing state on a loopback test host.

Tests/checks/README.md names pairing-sharing among the checks whose
counts came with their pull request, as main's last merges did for
theirs, and DEVELOPMENT.md's list of what the pure checks decide names
the pairing window and screen sharing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…affsanity#93, Saffsanity#151) into fix/remote-desktop-qr-code

Main brought Send Audio as a beta (Saffsanity#94), Reverse Scrolling (Saffsanity#159),
SILL_TEST_NO_SCREEN (Saffsanity#227), the Close timeout and the interrupted picture
(Saffsanity#93) and a sleeping display woken for a device (Saffsanity#151). Six files
conflicted, each resolved keeping both sides:

- .github/workflows/ci.yml: the mutants matrix is main's 51 checks with
  pairing-sharing, sorted: 52, exactly the folders of Tests/checks with a
  mutants.py.
- Sources/SillMenuBar/HostSettings.swift: Saffsanity#94's sendAudio beside
  showPairingWindowToScreenSharing in Key and in the registered defaults
  (sendAudio first, a HostConfig knob next to shareClipboard), as Noah's
  comment of 2026-10-19 settled the overlap: whichever lands second keeps
  both. HostConfig's sendAudio, read and saved, and this switch's property
  merged by themselves.
- Sources/SillMenuBar/DebugHooks.swift: -SillSetAfter's list names both
  keys (sendAudio, and showPairingWindowToScreenSharing=0|1; shareClipboard,
  which both sides had added, once); both cases of apply merged by
  themselves.
- Tests/checks/README.md: main's rewrapped introduction with this PR's
  "whether screen sharing shows the pairing window"; the pairing-sharing
  row beside main's phone-portrait and pointer-control rows; the history's
  list of checks and their pull requests ends with pairing-sharing and Saffsanity#157.
- docs/release-checklist.md: the marker paragraph names every marker once
  (Clipboard, Keyframe, Display wake, Sound, Reverse Scrolling, Pairing
  window), the screen sharing grep among those that print nothing, and all
  six perl lines.
- CLAUDE.md: both Current step entries, this PR's first, then main's
  three; Build and run's defaults list with sendAudio and
  showPairingWindowToScreenSharing; this PR's entry names this merge, and
  its verification is this merge's (Tests/checks/run-all.sh, all 52; the
  previews against main's 74d099c, the Devices pane's eight differ, two
  new, the other 136 byte for byte).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Saffsanity
Saffsanity merged commit 76bc100 into Saffsanity:main Oct 27, 2026
2 checks passed
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
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>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 27, 2026
Both sides kept where they met. CLAUDE.md's Current step has Saffsanity#147's
entry, then Saffsanity#157's (the pairing window to screen sharing), then the
sound's; Saffsanity#147's header now says main at 76bc100. Layout, the checks list
and the defaults list merged by themselves: PairingWindowSharing,
pairing-sharing and showPairingWindowToScreenSharing beside
TextComposition and text-composition.

CI's mutants matrix is the union, sorted: 53 checks, pairing-sharing
from main and text-composition from here, exactly the folders with a
mutants.py. The checks README's lede names what an input method or
dictation sends the Mac and whether screen sharing shows the pairing
window; both rows and both history clauses merged by themselves.
docs/DEVELOPMENT.md's pure-checks paragraph names both too; its
"Through Screen Sharing" bullet and "Other languages and dictation"
merged by themselves.

docs/release-checklist.md's paragraph on marked words names every
marker once (Clipboard, Keyframe, Display wake, Other languages, Sound,
Reverse Scrolling, Pairing window) with every grep, the dictation one
and the screen sharing one among them; the perl lines, one per marker,
merged by themselves. The cuts tried on a copy of site/ leave the greps
only privacy.html's clipboard log line. README.md's How it works and
Pairing, and site/support.html's Other languages paragraph and Pairing
window item, each inside its own markers, merged by themselves. The
next What's New is unchanged at 1,571 characters (Saffsanity#157 is the Mac's
alone); privacy.html was changed by neither side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Only CI's mutants matrix conflicted: both new checks kept, in order
(capture-scale, pairing-sharing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
…ring) into goodbye-sleep

Main moved on while the review's fixes were made. Only CLAUDE.md
conflicted: both Current step entries kept whole, this branch's first,
then Saffsanity#157's. DebugHooks.swift merged by itself, this branch's
-SillWakeAfter in the timed hooks beside Saffsanity#157's
showPairingWindowToScreenSharing; Tests/checks/README.md, DEVELOPMENT.md
and ci.yml's matrix (pairing-sharing added, still sorted) likewise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 27, 2026
Main moved from 74d099c (where this branch began) to 48a4ef3: #157, the
pairing window shown to screen sharing (Settings › Devices' switch,
PairingWindowSharing and its check), then #147, Chinese input and
dictation (TextComposition, the input view as a UITextInput, its check).
Neither touched DiscoveryPolicy.swift, StreamClient+Home.swift, any
other file the checks that compile DiscoveryPolicy.swift compile, or
those checks (home-device, home-model and policy with records.swift,
which this branch changed, and remote-rules).

Where they met:
- CLAUDE.md: both sides put an entry at the top of Current step. All
  three kept, this branch's first (a Mac's row keeps its TXT record when
  the cable goes), then main's two in main's order (#147, then #157).
  Both sides' Layout lines (DiscoveryPolicy's rowRecords; TextComposition,
  PairingWindowSharing and the two new checks) and main's key in the
  defaults list (showPairingWindowToScreenSharing) merged by themselves.
- Tests/checks/README.md: main re-wrapped the history paragraph's last
  sentence (after adding "pairing-sharing with #157"), where this branch
  had appended its own sentence (home-device, home-model and policy grew
  with cable-unplug-row): main's wrap, then this branch's sentence. The
  table's rows (this branch's home-device, home-model and policy; main's
  pairing-sharing and text-composition), main's lede and its
  text-composition paragraph merged by themselves.
- Merged by themselves: iOSClient/StreamClient.swift (this branch's
  recomputeMacs reading rows through networkRecords and nearbyRecords and
  record(of:) in place of tag(of:) and door(of:); main's onCaretMayMove and
  sendAfterInput, apart from it; no new caller of the two functions this
  branch removed) and docs/DEVELOPMENT.md (this branch's bullet in Pairing
  at home, beside #157's sentences there).
- One side's alone: ci.yml's mutants matrix is main's (53, exactly the
  folders with a mutants.py, sorted: pairing-sharing and text-composition
  added; this branch added no folder); docs/release-checklist.md and site/
  are main's (each marker once, with its perl line and grep).

Verified on this merge (Xcode 27.1): Tests/checks/run-all.sh, all 53; the
mutants of the four checks that compile DiscoveryPolicy.swift, every one
caught: home-device 203 checks and 129 of 129, home-model 73 and 24 of
24, policy 458 and 163 of 163 (this branch's T1-T5 on rowRecords among
them), remote-rules 144 and 76 of 76; swift build -c release, only the
CaptureProbe warning; the iOS app, Debug for the generic simulator as
CI builds it (CODE_SIGNING_ALLOWED=NO ARCHS=arm64), only the known
StreamClient capture warning (StreamClient.swift:3899).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 27, 2026
Main moved again, from 48a4ef3 (merged in just before) to b4caa91: #233,
capture at the display's own pixel scale (CaptureScale and its check;
HostConfig, InputInjector, StreamCoordinator and WindowCapture). Host
only, as this branch is device only: no Swift file on both sides
(Sources/ and Package.swift are main's, iOSClient/ this branch's).

Where they met:
- CLAUDE.md: both sides put an entry at the top of Current step again.
  This branch's first, then #233's, then #147's and #157's as before.
  #233's Layout lines merged by themselves beside this branch's.
- Tests/checks/README.md: #233's lede words and its capture-scale row
  merged by themselves beside this branch's rows (home-device,
  home-model, policy) and its history sentence.
- docs/DEVELOPMENT.md merged by itself (#233's words on the capture's
  scale; this branch's bullet in Pairing at home).
- ci.yml's mutants matrix is main's: 54, capture-scale added, exactly
  the folders with a mutants.py, sorted (this branch added none).
  docs/release-checklist.md and site/ are main's, untouched by #233.

Verified on this merge (Xcode 27.1): Tests/checks/run-all.sh, all 54
(capture-scale 79 among them; home-device 203, home-model 73, policy
458, remote-rules 144); swift build -c release from a git archive of
this tree, only the CaptureProbe warning; the iOS app, Debug for the
generic simulator as CI builds it (CODE_SIGNING_ALLOWED=NO ARCHS=arm64),
only the known StreamClient capture warning (StreamClient.swift:3899).
#233 changed no file that the four checks compiling DiscoveryPolicy.swift
compile, so their mutants on the merge before this one (392, every one
caught) stand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Saffsanity

Copy link
Copy Markdown
Owner

Thank you @scripsweave, your opt-in switch is in the Mac nightly: Settings › Devices › "Show the pairing window to screen sharing" lets a headless Mac pair through Screen Sharing or Apple Remote Desktop, and I've let the folks on #108 and #137 know. Sill for iPhone and iPad 0.5.5 (5) is on TestFlight too; nightlies build as 0.5.5 with rising build numbers and Sill's update check never offers them, and 0.5.1 stays the supported release. Please tell me here how it goes on your setup!

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.

Pairing code doesn't display when accessing a headless Mac

2 participants