5 Commits

Author SHA1 Message Date
enricobuehler 46b9aa8cf0 fix(windows-host): IDD activation — resolve-first, force-EXTEND only as fallback
apple / swift (push) Successful in 1m6s
android / android (push) Successful in 4m23s
ci / rust (push) Successful in 5m9s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 57s
apple / screenshots (push) Successful in 5m33s
windows-host / package (push) Successful in 8m46s
deb / build-publish (push) Successful in 3m13s
decky / build-publish (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
ci / bench (push) Successful in 4m51s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m15s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m19s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m44s
force_extend_topology() was added before the resolve loop to de-clone a fresh IDD on
integrated-screen boxes (laptops), but its bare SDC_TOPOLOGY_EXTEND preset is
ACCESS_DENIED from the Session-0 service context on a HEADLESS box and broke the IDD
auto-activation there: resolve_gdi_name stayed None -> "not an active display path" ->
black screen. That regressed the headless/primary platform (live RTX box).

Revert to the proven e2c9bfd flow: resolve FIRST (Windows auto-activates the IDD as its
own extended path), and force-EXTEND only as the FALLBACK when resolve returns None (the
integrated-screen clone case, observed live to leave resolve None). The success path is
byte-identical to e2c9bfd (resolve -> set_active_mode -> isolate_displays_ccd).

Validated live: the headless RTX box streams again (probe: frames flow, driver attaches
to the ring, host/driver render LUIDs match).

Reviewed multi-agent + adversarial: no regression on the validated headless path or the
observed Optimus-laptop clone path (a cloned IddCx target resolves to None there, so the
is_none() fallback fires + de-clones). Known theoretical caveat, documented inline and
unobserved for IddCx but untested across GPU/driver/OS: a CCD clone that manifests as a
shared-source ACTIVE path would resolve to Some and bypass the is_none() gate. Follow-up:
widen the gate (a target_is_cloned helper) once an integrated-screen box is available to
validate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:15:48 +02:00
enricobuehler 372b27540b fix(apple): render Acknowledgements notices in lazy chunks
apple / swift (push) Successful in 1m11s
android / android (push) Successful in 4m19s
ci / web (push) Successful in 51s
ci / rust (push) Successful in 5m14s
ci / docs-site (push) Successful in 58s
release / apple (push) Successful in 7m54s
deb / build-publish (push) Successful in 4m0s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
ci / bench (push) Successful in 5m19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
apple / screenshots (push) Successful in 5m37s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m26s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m35s
docker / deploy-docs (push) Successful in 18s
THIRD-PARTY-NOTICES.txt is ~885 KB / 16k lines; rendering it in a single
SwiftUI Text overshot the text-rendering height limit — it laid out for ages
and drew blank below the cutoff (only the small punktfunk licenses above it
showed). Split the notices into ~80 line-chunks (<=200 lines / <=18 KB each,
computed once as Licenses.thirdPartyNoticesChunks) and render them in a
top-level LazyVStack so only on-screen chunks lay out and no chunk is tall
enough to clip. Chunking is lossless — rejoining the chunks reproduces the
original byte-for-byte, so no notice text is dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:34:19 +02:00
enricobuehler db4d15bf8b fix(apple): stop the iOS/iPadOS Add Host sheet from scrolling
apple / swift (push) Successful in 1m5s
android / android (push) Successful in 4m15s
ci / rust (push) Successful in 4m56s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 58s
deb / build-publish (push) Successful in 3m11s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
release / apple (push) Successful in 8m32s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m40s
apple / screenshots (push) Successful in 5m44s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m29s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m31s
docker / deploy-docs (push) Successful in 17s
The add-host content is a SwiftUI Form (backed by a scrollable list), so it
bounced/scrolled inside the fixed .height(320) detent even though the three
rows + action button fit exactly. Lock it with .scrollDisabled(true) on iOS
(covers iPadOS); macOS (fixed-size panel) and tvOS (custom rows, no Form) are
untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:12:08 +02:00
enricobuehler 8e24ea9ed7 fix(ci): archive Apple release builds with Automatic signing
The in-app OSS license screens (7591425) added a `resources:` array to the
PunktfunkKit SwiftPM target, which makes SwiftPM emit a resource-bundle target
(PunktfunkKit_PunktfunkKit). A resource bundle is a product type that cannot
carry a provisioning profile, so the explicit PROVISIONING_PROFILE_SPECIFIER
each release.yml archive step set — global on macOS, sdk-scoped on iOS/tvOS —
now lands on it and fails the archive ("does not support provisioning profiles")
on all three platforms. (Before that commit there was no resource bundle, so the
profile was harmless.)

Switch all three archive steps to CODE_SIGN_STYLE=Automatic (development):
Automatic signing assigns a profile only to the app target and leaves the
resource bundle (and the macOS-host SwiftPM macro plugins) alone, and bakes the
sandbox entitlements in. No -allowProvisioningUpdates, so it stays offline and
never cloud-signs (the App-Manager ASC key can't). DISTRIBUTION signing is
unchanged — still manual, in the -exportArchive step (which maps the profile to
io.unom.punktfunk only). Drops the now-unneeded manual signing xcconfigs.

Requires the runner to have a development provisioning profile for
io.unom.punktfunk on each platform (now installed for macOS/iOS/tvOS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:12:08 +02:00
enricobuehler 73c0125843 fix(mgmt): regenerate api/openapi.json for 0.3.0
apple / swift (push) Successful in 1m6s
android / android (push) Successful in 4m16s
ci / rust (push) Successful in 5m5s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 58s
apple / screenshots (push) Successful in 5m17s
deb / build-publish (push) Successful in 3m11s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
ci / bench (push) Successful in 4m43s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 26s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m25s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m28s
docker / deploy-docs (push) Successful in 17s
The OpenAPI 'info.version' tracks CARGO_PKG_VERSION; the 0.3.0 bump made the
checked-in spec stale (the openapi_document_is_complete_and_checked_in test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 07:54:30 +00:00
6 changed files with 134 additions and 85 deletions
+40 -48
View File
@@ -207,10 +207,20 @@ jobs:
# (Config/Punktfunk-macOS.entitlements) — mandatory for the Mac App Store. # (Config/Punktfunk-macOS.entitlements) — mandatory for the Mac App Store.
continue-on-error: true continue-on-error: true
run: | run: |
# Separate archive from the Developer ID one above: App Store needs a profile-signed # Separate archive from the Developer ID one above: App Store needs a signed, entitled
# archive (manual signing), not the unsigned-then-codesign DMG path. Same App-Manager # archive that -exportArchive can re-sign for distribution, not the unsigned-then-codesign
# ASC-key constraint as iOS/tvOS — MANUAL signing, NOT -allowProvisioningUpdates # DMG path. Archive with AUTOMATIC signing (development). Why not a manually-specified
# (cloud signing the key can't do). Quit Xcode so it can't prune the dropped profile. # profile (as this step used to do): the in-app license screens added a SwiftPM resource
# bundle (PunktfunkKit_PunktfunkKit), and a resource bundle is a product type that cannot
# carry a provisioning profile — a global PROVISIONING_PROFILE_SPECIFIER (here) or an
# sdk-scoped one (iOS/tvOS) lands on it and fails the archive ("does not support
# provisioning profiles"). Automatic signing assigns a profile only to the app and leaves
# the resource bundle (and the macOS-host macro plugins) alone, and bakes the sandbox
# entitlements in. No -allowProvisioningUpdates → it stays OFFLINE and never cloud-signs
# (the App-Manager ASC key can't), so the runner must have a macOS *development* profile
# for io.unom.punktfunk installed. DISTRIBUTION signing happens in the export step below
# (manual, via the plist). Quit Xcode so it can't prune the manually-installed App Store
# distribution profile that export needs.
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
pkill -x Xcode 2>/dev/null || true pkill -x Xcode 2>/dev/null || true
PROFILE="Punktfunk macOS App Store Distribution" PROFILE="Punktfunk macOS App Store Distribution"
@@ -218,11 +228,10 @@ jobs:
-project "$PROJECT" -scheme Punktfunk \ -project "$PROJECT" -scheme Punktfunk \
-destination 'generic/platform=macOS' \ -destination 'generic/platform=macOS' \
-archivePath "$RUNNER_TEMP/Punktfunk-macos-appstore.xcarchive" \ -archivePath "$RUNNER_TEMP/Punktfunk-macos-appstore.xcarchive" \
-skipMacroValidation -skipPackagePluginValidation \
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \ MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
CODE_SIGN_STYLE=Manual \ CODE_SIGN_STYLE=Automatic \
CODE_SIGN_IDENTITY="Apple Distribution" \ DEVELOPMENT_TEAM="$TEAM_ID"
DEVELOPMENT_TEAM="$TEAM_ID" \
PROVISIONING_PROFILE_SPECIFIER="$PROFILE"
cat > "$RUNNER_TEMP/export-macos-appstore.plist" <<EOF cat > "$RUNNER_TEMP/export-macos-appstore.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -252,35 +261,27 @@ jobs:
# Best-effort until the App Store Connect app record for io.unom.punktfunk exists. # Best-effort until the App Store Connect app record for io.unom.punktfunk exists.
continue-on-error: true continue-on-error: true
run: | run: |
# MANUAL App Store signing: the local (valid) Apple Distribution identity + the App # Archive with AUTOMATIC signing (development) — see the macOS App Store step for the full
# Store provisioning profile. NOT -allowProvisioningUpdates — with an App-Manager-role # rationale. The SwiftPM resource bundle (PunktfunkKit_PunktfunkKit, added with the in-app
# ASC key that forces Xcode's CLOUD-managed signing, which the role can't do ("Cloud # license screens) builds for iphoneos, so even the sdk-scoped PROVISIONING_PROFILE_SPECIFIER
# signing permission error"). The profile must be installed on the runner under # this step used to set matched it and failed the archive ("does not support provisioning
# ~/Library/Developer/Xcode/UserData/Provisioning Profiles/ (install it once with # profiles"). Automatic signing profiles only the app and leaves the resource bundle (and
# Xcode.app quit, or it prunes the manually-dropped distribution profile). # the macOS-host macro plugins) alone. No -allowProvisioningUpdates → OFFLINE, never
# A running Xcode.app prunes unrecognized profiles from that dir — quit it so the App # cloud-signs (the App-Manager ASC key can't), so the runner needs an iOS *development*
# Store profile survives this build; headless xcodebuild doesn't need the GUI app. # profile for io.unom.punktfunk installed. DISTRIBUTION signing is the export step below
# (manual, via the plist). A running Xcode.app prunes unrecognized profiles — quit it so the
# manually-installed App Store distribution profile survives for export.
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
pkill -x Xcode 2>/dev/null || true pkill -x Xcode 2>/dev/null || true
PROFILE="Punktfunk iOS App Store Distribution" PROFILE="Punktfunk iOS App Store Distribution"
# Scope signing to the iOS device SDK via an xcconfig — see the tvOS step below for the
# full rationale. A global (CLI) profile specifier would also be forced onto the shared
# macOS-host SwiftPM macro plugins, which reject it and fail the archive; [sdk=iphoneos*]
# in an xcconfig lands it on the app/framework slices only.
SIGN_XCCONFIG="$RUNNER_TEMP/sign-ios.xcconfig"
cat > "$SIGN_XCCONFIG" <<XCCONF
CODE_SIGN_STYLE = Manual
DEVELOPMENT_TEAM = $TEAM_ID
CODE_SIGN_IDENTITY[sdk=iphoneos*] = Apple Distribution
PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = $PROFILE
XCCONF
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \ DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
-project "$PROJECT" -scheme Punktfunk-iOS \ -project "$PROJECT" -scheme Punktfunk-iOS \
-destination 'generic/platform=iOS' \ -destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/Punktfunk-ios.xcarchive" \ -archivePath "$RUNNER_TEMP/Punktfunk-ios.xcarchive" \
-skipMacroValidation -skipPackagePluginValidation \ -skipMacroValidation -skipPackagePluginValidation \
-xcconfig "$SIGN_XCCONFIG" \ MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM="$TEAM_ID"
cat > "$RUNNER_TEMP/export-appstore.plist" <<EOF cat > "$RUNNER_TEMP/export-appstore.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -312,33 +313,24 @@ jobs:
# on the runner (xcodebuild -downloadPlatform tvOS). # on the runner (xcodebuild -downloadPlatform tvOS).
continue-on-error: true continue-on-error: true
run: | run: |
# Same manual App Store signing as iOS (the App-Manager ASC key can't cloud-sign). # Archive with AUTOMATIC signing (development) — see the macOS App Store step. The SwiftPM
# resource bundle (PunktfunkKit_PunktfunkKit) builds for appletvos and rejected the
# sdk-scoped profile this step used to set; Automatic signing profiles only the app and
# leaves the resource bundle + the macOS-host macro plugins (OnceMacro/SwizzlingMacro/
# AssociationMacro) alone. No -allowProvisioningUpdates → OFFLINE, never cloud-signs (the
# App-Manager ASC key can't), so the runner needs a tvOS *development* profile for
# io.unom.punktfunk installed. DISTRIBUTION signing is the export step below (manual, plist).
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
pkill -x Xcode 2>/dev/null || true pkill -x Xcode 2>/dev/null || true
PROFILE="Punktfunk tvOS App Store Distribution" PROFILE="Punktfunk tvOS App Store Distribution"
# Scope signing to the tvOS device SDK via an xcconfig. A global (CLI) profile specifier
# hits EVERY target, including the shared SwiftPM macro plugins (OnceMacro/SwizzlingMacro/
# AssociationMacro) which build for the macOS host and reject a provisioning profile
# ("<macro> does not support provisioning profiles"), failing the archive. Conditionals
# work only in an xcconfig (xcodebuild mis-parses a CLI "SETTING[sdk=..]=val"), and a
# command-line -xcconfig outranks target settings, so [sdk=appletvos*] puts the profile on
# the app/framework slices only — the macosx-host macros get nothing. (The macOS archive
# above is immune: its host-SDK macros are CODE_SIGNING_ALLOWED=NO, so a global specifier
# is ignored there.)
SIGN_XCCONFIG="$RUNNER_TEMP/sign-tvos.xcconfig"
cat > "$SIGN_XCCONFIG" <<XCCONF
CODE_SIGN_STYLE = Manual
DEVELOPMENT_TEAM = $TEAM_ID
CODE_SIGN_IDENTITY[sdk=appletvos*] = Apple Distribution
PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*] = $PROFILE
XCCONF
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \ DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
-project "$PROJECT" -scheme Punktfunk-tvOS \ -project "$PROJECT" -scheme Punktfunk-tvOS \
-destination 'generic/platform=tvOS' \ -destination 'generic/platform=tvOS' \
-archivePath "$RUNNER_TEMP/Punktfunk-tvos.xcarchive" \ -archivePath "$RUNNER_TEMP/Punktfunk-tvos.xcarchive" \
-skipMacroValidation -skipPackagePluginValidation \ -skipMacroValidation -skipPackagePluginValidation \
-xcconfig "$SIGN_XCCONFIG" \ MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM="$TEAM_ID"
cat > "$RUNNER_TEMP/export-tvos.plist" <<EOF cat > "$RUNNER_TEMP/export-tvos.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+1 -1
View File
@@ -10,7 +10,7 @@
"name": "MIT OR Apache-2.0", "name": "MIT OR Apache-2.0",
"identifier": "MIT OR Apache-2.0" "identifier": "MIT OR Apache-2.0"
}, },
"version": "0.0.1" "version": "0.3.0"
}, },
"paths": { "paths": {
"/api/v1/clients": { "/api/v1/clients": {
@@ -10,6 +10,11 @@ struct AcknowledgementsView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
// Top-level LazyVStack so the third-party-notices chunks (Licenses.thirdPartyNoticesChunks,
// ~885 KB total) load lazily as they scroll into view a single Text that large overshoots
// the text-rendering height limit (blank below the limit + very slow). spacing 0 keeps the
// notice chunks visually continuous; the header block carries its own spacing + bottom pad.
LazyVStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 18) {
Text("punktfunk") Text("punktfunk")
.font(.title2).bold() .font(.title2).bold()
@@ -33,10 +38,17 @@ struct AcknowledgementsView: View {
) )
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(Licenses.thirdPartyNotices) }
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 18)
ForEach(Licenses.thirdPartyNoticesChunks.indices, id: \.self) { i in
Text(Licenses.thirdPartyNoticesChunks[i])
.font(.caption2.monospaced()) .font(.caption2.monospaced())
.frame(maxWidth: .infinity, alignment: .leading)
.modifier(SelectableText()) .modifier(SelectableText())
} }
}
.frame(maxWidth: 900, alignment: .leading) .frame(maxWidth: 900, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding() .padding()
@@ -80,6 +80,11 @@ struct AddHostSheet: View {
} }
#if !os(tvOS) #if !os(tvOS)
.formStyle(.grouped) .formStyle(.grouped)
#endif
#if os(iOS)
// The detent below is sized to fit all 3 rows + the action button exactly, so the
// Form must NOT scroll/bounce inside it lock it. (iOS 16+; safe at iOS 17.)
.scrollDisabled(true)
#endif #endif
#if os(macOS) #if os(macOS)
// macOS: UNCHANGED Cancel + Spacer + Add in an HStack, both wired to the // macOS: UNCHANGED Cancel + Spacer + Add in an HStack, both wired to the
@@ -120,8 +125,8 @@ struct AddHostSheet: View {
// Form + the full-width action row, instead of the half-screen .medium it used to rest // Form + the full-width action row, instead of the half-screen .medium it used to rest
// at. A single fixed detent is enough: the system keeps the content above the keyboard // at. A single fixed detent is enough: the system keeps the content above the keyboard
// when Address/Port is focused, and on iPadOS this renders as a short bottom sheet (not a // when Address/Port is focused, and on iPadOS this renders as a short bottom sheet (not a
// centered formSheet card). If Dynamic Type grows the rows past this height the Form just // centered formSheet card). The Form itself is .scrollDisabled (above) so it can't
// scrolls inside the detent nothing is clipped. (.height(_:) is iOS 16+, safe at iOS 17.) // bounce/scroll inside this fixed detent. (.height(_:) is iOS 16+, safe at iOS 17.)
.presentationDetents([.height(320)]) .presentationDetents([.height(320)])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
#endif #endif
@@ -33,4 +33,18 @@ public enum Licenses {
let text = resource("THIRD-PARTY-NOTICES") let text = resource("THIRD-PARTY-NOTICES")
return text.isEmpty ? "Third-party notices unavailable." : text return text.isEmpty ? "Third-party notices unavailable." : text
} }
/// `thirdPartyNotices` pre-split into render-sized line chunks. The full notices are ~885 KB /
/// 16k lines; a single SwiftUI `Text` that large overshoots CoreText/CoreAnimation's max
/// renderable height it lays out for ages and draws blank past the limit so the
/// Acknowledgements screen renders these chunks in a `LazyVStack` (only on-screen chunks lay
/// out, and no chunk is tall enough to clip). Split at line boundaries and joined with "\n";
/// the inter-chunk break is the `LazyVStack` row boundary, so no text is lost. Computed once.
public static let thirdPartyNoticesChunks: [String] = {
let lines = thirdPartyNotices.split(separator: "\n", omittingEmptySubsequences: false)
let chunkSize = 200
return stride(from: 0, to: lines.count, by: chunkSize).map { start in
lines[start..<min(start + chunkSize, lines.count)].joined(separator: "\n")
}
}()
} }
@@ -327,17 +327,17 @@ impl VirtualDisplayManager {
} }
}); });
// Windows defaults a new IddCx monitor into CLONE mode when a physical display is already // Resolve the capture target — wait for Windows to auto-activate the freshly-ADDed IDD into its
// active (a laptop panel, an attached monitor): the cloned IDD shares that display's source, so // OWN display path (it comes up EXTENDED alongside any existing/basic display; `set_active_mode`
// the OS never commits a distinct path for it and capture sees no frames. Force EXTEND first so // below then promotes it to primary and `isolate_displays_ccd` makes it the sole composited
// the IDD comes up as its OWN active path; the resolve loop below then finds it. Idempotent / // desktop — the proven flow). May be None on a GPU-less box (target added but not WDDM-activated);
// no-op on a sole-display box, so it's safe on the headless single-GPU path too.
// SAFETY: `force_extend_topology` only calls `SetDisplayConfig` (a CCD topology apply) with no
// borrowed caller memory; it runs under the manager `state` lock, the sole topology mutator.
unsafe { force_extend_topology() };
// Resolve the capture target. May be None on a GPU-less box (target added but not WDDM-activated);
// the capture backend re-resolves once a GPU is present. // the capture backend re-resolves once a GPU is present.
//
// We do NOT force a topology change FIRST: the bare `SDC_TOPOLOGY_EXTEND` preset is ACCESS_DENIED
// from our Session-0 service context on a headless box and BREAKS this auto-activate (it regressed
// the headless path — the IDD then never gets its own path → "not an active display path" → black).
// force-EXTEND is only the FALLBACK below, for an integrated-screen box where a fresh IDD is CLONED
// onto the panel (shares its source) instead of getting its own path.
let mut gdi_name = None; let mut gdi_name = None;
for _ in 0..15 { for _ in 0..15 {
thread::sleep(Duration::from_millis(200)); thread::sleep(Duration::from_millis(200));
@@ -349,6 +349,32 @@ impl VirtualDisplayManager {
break; break;
} }
} }
// Fallback for an integrated-screen box (e.g. a laptop panel): Windows CLONES a freshly-added
// IDD onto the existing display, sharing its source, so it never gets its own committed path. On
// the IddCx clone behaviour observed live (commit 8e87e61, an Intel-iGPU + NVIDIA-Optimus laptop)
// `resolve_gdi_name` then stays None — so this `is_none()` fallback fires, force-EXTENDs to
// de-clone, and the second resolve finds the now-committed path. Headless/extended boxes already
// resolved above (the IDD auto-activates with its OWN source) and skip this — which is the whole
// point, since force-EXTEND's bare preset is ACCESS_DENIED from our service context there.
//
// CAVEAT (unobserved for IddCx, untested across GPU/driver/OS): textbook CCD also lets a clone
// appear as a *shared-source ACTIVE* path (resolve → Some), which this `is_none()` gate would NOT
// catch. If that ever shows up, widen the gate to also fire when the IDD target's source is shared
// with another active path (a `target_is_cloned` helper) — needs on-laptop validation first.
if gdi_name.is_none() {
// SAFETY: as above — `force_extend_topology` only calls `SetDisplayConfig` (CCD) with no
// borrowed caller memory, under the `state` lock.
unsafe { force_extend_topology() };
for _ in 0..15 {
thread::sleep(Duration::from_millis(200));
// SAFETY: as the resolve loop above.
if let Some(n) = unsafe { resolve_gdi_name(added.target_id) } {
gdi_name = Some(n);
break;
}
}
}
let mut ccd_saved: Option<SavedConfig> = None; let mut ccd_saved: Option<SavedConfig> = None;
match &gdi_name { match &gdi_name {
Some(n) => { Some(n) => {