native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620import ComposableArchitectureimport SwiftUI
struct MirrorHostButton: View { @Environment(RemoteMirrorStore.self) private var mirrors @Environment(ToolbarPopoverCoordinator.self) private var popovers @Dependency(FeatureFlags.self) private var featureFlags @State private var sheet: MirrorSheet? @State private var connectionToOpen: MirrorKnownHost?
private enum MirrorSheet: String, Identifiable { case pairing, connect var id: String { rawValue } }
private var status: String { if !mirrors.host.isRunning { return String(localized: "Host off") } if mirrors.host.subscriberCount == 0 { return String(localized: "Host listening") } return String(localized: "Host mirroring") }
private var tint: Color { guard mirrors.host.isRunning else { return .secondary } return mirrors.host.subscriberCount == 0 ? .blue.opacity(0.65) : .green.opacity(0.65) }
var body: some View { if featureFlags.remoteMirror { let isPresented = popovers.presented == .mirror Button { popovers.toggle(.mirror) // Keychain is read only after a click; hover previews stay in memory. if popovers.presented == .mirror { mirrors.knownHosts.importLegacyIfNeeded() } } label: { Image(systemName: "network").foregroundStyle(tint) } .help("Remote Mirror — \(status). Hover to preview or click to keep open.") .accessibilityLabel("Remote Mirror — \(status)") .accessibilityIdentifier("remote-mirror-host-button") .onHover { if sheet == nil { popovers.hoverButton(.mirror, hovering: $0) } } .popover( isPresented: Binding( get: { isPresented }, set: { if !$0 { popovers.dismiss(.mirror) } } ) ) { MirrorPopover { MirrorSettingsView( host: mirrors.host, interact: { popovers.pin(.mirror) }, pair: { present(.pairing) }, connect: { connectionToOpen = nil present(.connect) }, reconnect: { connectionToOpen = $0 present(.connect) } ) .fixedSize(horizontal: false, vertical: true) } .onHover { popovers.hoverPopover(.mirror, hovering: $0) } } .background { Color.clear .sheet(item: $sheet) { destination in switch destination { case .pairing: MirrorPairingView(host: mirrors.host) { sheet = nil } case .connect: AddRemoteMirrorView(dismiss: { sheet = nil }, savedHost: connectionToOpen) } } } .onDisappear { popovers.dismiss(.mirror) } } }
private func present(_ destination: MirrorSheet) { popovers.dismiss(.mirror) sheet = destination }}
private struct MirrorPopover<Content: View>: View { @ViewBuilder let content: () -> Content @State private var contentHeight: CGFloat = 360
var body: some View { ScrollView { content() .onGeometryChange(for: CGFloat.self) { $0.size.height } action: { contentHeight = $0 } } .frame(width: 460, height: min(contentHeight, max(240, (NSScreen.main?.visibleFrame.height ?? 840) - 180))) .transaction { $0.animation = nil } }}
private struct MirrorSettingsView: View { @Environment(RemoteMirrorStore.self) private var mirrors @Bindable var host: MirrorHost let interact: () -> Void let pair: () -> Void let connect: () -> Void let reconnect: (MirrorKnownHost) -> Void @State private var interfaces: [MirrorNetworkInterface] = [] @State private var deviceToRevoke: MirrorPairedDevice? @State private var revokeAllPresented = false @State private var mirrorToDisconnect: DisconnectTarget?
private struct DisconnectTarget { let subscriptionID: UUID let paneTitle: String let deviceName: String }
@State private var hostToRename: MirrorKnownHost? @State private var alias = ""
private var listenOptions: [ListenOption] { var options = [ ListenOption(address: MirrorHostAddresses.allIPv4, label: String(localized: "All interfaces (0.0.0.0)")) ] for interface in interfaces where interface.isIPv4 && !interface.isLoopback { options.append(ListenOption(address: interface.address, label: interface.label + " · " + interface.address)) } options.append( ListenOption(address: MirrorHostAddresses.loopback, label: String(localized: "This Mac only (127.0.0.1)")) ) if !options.contains(where: { $0.address == host.address }) { options.append(ListenOption(address: host.address, label: String(localized: "Custom · \(host.address)"))) } return options }
private var hostStatus: String { if host.isStarting { return String(localized: "Starting…") } guard host.isRunning else { return String(localized: "Host is off") } return String(localized: "Listening · \(Self.mirrors(host.subscriberCount))") }
var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .firstTextBaseline) { Text("Remote Mirror").font(.headline) Spacer() Text(hostStatus).font(.subheadline).foregroundStyle(.secondary) } hostSection Divider() clientSection } .padding(20) .frame(width: 460) .accessibilityIdentifier("remote-mirror-host-panel") .onAppear { interfaces = MirrorHostAddresses.current() } .onChange(of: host.address) { _, _ in interact() } .onChange(of: host.port) { _, _ in interact() } .confirmationDialog( "Revoke \(deviceToRevoke?.name ?? "this device")?", isPresented: Binding(get: { deviceToRevoke != nil }, set: { if !$0 { deviceToRevoke = nil } }), presenting: deviceToRevoke ) { device in Button("Revoke", role: .destructive) { host.revoke(device.id) } } message: { _ in Text("The device is disconnected and must pair again before it can mirror this Mac.") } .alert( "Revoke All Devices?", isPresented: $revokeAllPresented ) { Button("Revoke All Devices", role: .destructive) { host.revokeAllDevices() } Button("Cancel", role: .cancel) {} } message: { Text( """ All devices will be disconnected and must pair again. \ Saved connections and the current pairing code will no longer work. Local terminals keep running. """ ) } .alert( "Disconnect Mirror?", isPresented: Binding(get: { mirrorToDisconnect != nil }, set: { if !$0 { mirrorToDisconnect = nil } }), presenting: mirrorToDisconnect ) { target in Button("Disconnect", role: .destructive) { host.disconnect(subscriptionID: target.subscriptionID) } Button("Cancel", role: .cancel) {} } message: { target in Text( """ Disconnect \(target.deviceName) from \(target.paneTitle)? \ The terminal keeps running. The device stays paired and can reconnect. """) } .alert( "Rename Host", isPresented: Binding(get: { hostToRename != nil }, set: { if !$0 { hostToRename = nil } }), presenting: hostToRename ) { known in TextField("Name", text: $alias, prompt: Text(known.endpointID)) Button("Save") { mirrors.knownHosts.rename(known.id, alias: alias) } Button("Cancel", role: .cancel) {} } message: { known in Text("The name is shown instead of \(known.endpointID) on this Mac only.") } }
private var hostSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Host").font(.subheadline.weight(.semibold)) Text("Let paired Prowl apps on your network mirror this Mac’s terminals.") .foregroundStyle(.secondary) // Static text while running: a disabled, still-focused field keeps its selection highlight. let editable = !(host.isRunning || host.isStarting) // Center rows: the pop-up button reports no usable text baseline, so baseline alignment floats the label. Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 10) { GridRow { Text("Listen on").gridColumnAlignment(.trailing) if editable { // SwiftUI's menu picker keeps its intrinsic width and grows after its first open; // an AppKit pop-up button fills the row from the start. ListenAddressPopUp(options: listenOptions, selection: $host.address) .frame(maxWidth: .infinity) .help("Which of this Mac’s addresses accept connections") .accessibilityLabel("Listen on") } else { Text(listenOptions.first { $0.address == host.address }?.label ?? host.address) } } GridRow { Text("Port").gridColumnAlignment(.trailing) if editable { TextField("Port", text: $host.port) .labelsHidden() .frame(width: 100) .help("The network port other Prowl apps connect to") } else { Text(host.port) } } } HStack { Spacer() if host.isRunning || host.isStarting { Button("Add a Device…", action: pair) .help("Show a single-use pairing code with a 60-second expiry") .disabled(host.isStarting) Button("Stop Host", role: .destructive) { interact() host.stop() } .help("Disconnect mirrors and stop listening; local terminals keep running") } else { Button("Start Host") { interact() host.start() } .buttonStyle(.borderedProminent) .help("Allow paired Prowl devices to connect to this Mac") } } if !host.devices.isEmpty { Button("Revoke All Devices…", role: .destructive) { interact() revokeAllPresented = true } .help( "Disconnect all devices and require fresh pairing, including devices currently offline" ) .accessibilityIdentifier("remote-mirror-revoke-all-devices") } ForEach(host.devices) { device in VStack(alignment: .leading, spacing: 4) { let panes = host.mirroredPanes(for: device.id) HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) { Text(device.name).lineLimit(1) Text(deviceStatus(device, mirrorCount: panes.count)).font(.caption).foregroundStyle(.secondary) } Spacer() Button("Revoke", role: .destructive) { interact() deviceToRevoke = device } .help("Disconnect this device and require it to pair again") } ForEach(panes) { pane in HStack(spacing: 6) { Circle().fill(.green).frame(width: 6, height: 6) .accessibilityHidden(true) Text(pane.title).font(.caption).foregroundStyle(.secondary).lineLimit(1) .help(pane.title + "\n" + pane.directory) .accessibilityLabel("Mirroring \(pane.title)") Spacer(minLength: 4) Button { guard let subscriptionID = host.subscriptionID(for: pane.id, deviceID: device.id) else { return } interact() mirrorToDisconnect = DisconnectTarget( subscriptionID: subscriptionID, paneTitle: pane.title, deviceName: device.name) } label: { Image(systemName: "personalhotspot.slash") .font(.caption) .symbolRenderingMode(.monochrome) .foregroundStyle(.red) .frame(width: 18, height: 18) .contentShape(Rectangle()) } .buttonStyle(.borderless) .help("Disconnect this mirror; keep the device paired and the terminal running") .accessibilityLabel("Disconnect mirror of \(pane.title) from \(device.name)") .accessibilityIdentifier("disconnect-mirror-\(pane.id)") } } } } if let error = host.error { Text(error).foregroundStyle(.red).textSelection(.enabled) } Text("Closing this panel keeps Host running.").font(.caption).foregroundStyle(.secondary) } }
private var clientSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Client").font(.subheadline.weight(.semibold)) Text("Mirror a terminal pane from Prowl on another Mac.") .foregroundStyle(.secondary) ForEach(mirrors.knownHosts.hosts) { known in HStack { VStack(alignment: .leading, spacing: 4) { Text(known.displayName).lineLimit(1) Text(knownHostCaption(known)).font(.caption).foregroundStyle(.secondary).lineLimit(1) } Spacer() Button("Connect") { reconnect(known) } .help("Connect to this Host and select a pane") Menu { Button("Rename…") { interact() alias = known.alias ?? "" hostToRename = known } Button("Forget", role: .destructive) { interact() mirrors.knownHosts.forget(known.id) } } label: { Label("More", systemImage: "ellipsis.circle").labelStyle(.iconOnly) } .menuStyle(.borderlessButton) .menuIndicator(.hidden) .fixedSize() .help("Rename this Host or forget its saved access") } } if mirrors.knownHosts.hosts.isEmpty, mirrors.knownHosts.hasImportedLegacy, mirrors.knownHosts.notice == nil { Text("No paired Hosts yet.").font(.caption).foregroundStyle(.secondary) } if !mirrors.knownHosts.hasImportedLegacy, mirrors.knownHosts.notice == nil { Text("Hosts paired with an earlier version appear after you click the Remote Mirror button.") .font(.caption).foregroundStyle(.secondary) } if let notice = mirrors.knownHosts.notice { Text(notice).font(.caption).foregroundStyle(.secondary) if !mirrors.knownHosts.hasImportedLegacy { Button("Try Again") { interact() mirrors.knownHosts.importLegacyIfNeeded() } .help("Read previously paired Hosts from secure storage again") } } Button("Connect to a New Host…", action: connect) .help("Enter another Mac’s address and its pairing code") } }
private func deviceStatus(_ device: MirrorPairedDevice, mirrorCount: Int) -> String { if host.isOnline(device.id) { return String(localized: "Connected · \(Self.mirrors(mirrorCount))") } guard let seen = device.lastSeen else { return String(localized: "Paired · never connected") } return String(localized: "Last seen \(seen.formatted(.relative(presentation: .named)))") }
private func knownHostCaption(_ known: MirrorKnownHost) -> String { var parts: [String] = [] if known.alias != nil { parts.append(known.endpointID) } if let connected = known.lastConnectedAt { parts.append(String(localized: "Last connected \(connected.formatted(.relative(presentation: .named)))")) } else { parts.append(String(localized: "Never connected")) } return parts.joined(separator: " · ") }
private static func mirrors(_ count: Int) -> String { count == 1 ? String(localized: "1 mirror") : String(localized: "\(count) mirrors") }}
private struct MirrorPairingView: View { @Bindable var host: MirrorHost let dismiss: () -> Void @State private var qrAddress = "" @State private var hasRequestedCode = false @State private var copied = false @State private var copyError: String? @State private var interfaces: [MirrorNetworkInterface] = [] @State private var dismissTask: Task<Void, Never>?
private var reachable: [MirrorNetworkInterface] { MirrorHostAddresses.reachable(listenAddress: host.address, interfaces: interfaces) }
private var pairingAddress: String? { let choices = reachable.filter { !$0.isLoopback && $0.address != "::1" } return choices.first(where: { $0.address == qrAddress })?.address ?? choices.first?.address }
var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Add a Device").font(.title2.bold()) if let paired = host.lastPairedDevice { Label("Paired with \(paired.name)", systemImage: "checkmark.circle.fill") .font(.title3).foregroundStyle(.green) Text("The device can now connect to this Mac and select a pane.").foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } else { // Sheets propose no height; without fixedSize multi-line text collapses to one truncated line. Text( """ On the other device, open Remote Mirror → Client → Connect to a New Host. \ Copy the connection details and paste into any connection field on Mac, or scan the QR code on mobile. """ ) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) code pairingQR } if let error = copyError ?? host.error { Text(error).foregroundStyle(.red).fixedSize(horizontal: false, vertical: true) } HStack { Button(host.lastPairedDevice == nil ? "Cancel" : "Done") { host.cancelPairing() dismiss() } .keyboardShortcut(.cancelAction) .help("Close this window and invalidate the unused code") Spacer() if host.lastPairedDevice == nil { if !host.isRunning && !host.isStarting { Button("Start Host") { host.start() } .buttonStyle(.borderedProminent) .help("Start listening, then create a pairing code") } else { Button("Refresh Code") { host.addDevice() } .disabled(host.isStarting || !host.isRunning) .help("Invalidate the old code and create a new 60-second code") } } } } .padding(24).frame(width: 440) .onAppear { interfaces = MirrorHostAddresses.current() } .onChange(of: host.isStarting, initial: true) { _, starting in guard !hasRequestedCode, !starting, host.isRunning else { return } hasRequestedCode = true host.addDevice() } .onChange(of: qrAddress) { _, _ in copied = false } .onChange(of: host.pairingKey) { _, _ in copied = false copyError = nil } .onChange(of: host.lastPairedDevice) { _, paired in guard paired != nil else { return } dismissTask?.cancel() dismissTask = Task { try? await Task.sleep(for: .seconds(1.5)) guard !Task.isCancelled else { return } dismiss() } } .onDisappear { dismissTask?.cancel() host.cancelPairing() } }
private var pairingQR: some View { let choices = reachable.filter { !$0.isLoopback && $0.address != "::1" } let selected = pairingAddress return VStack(spacing: 8) { if let expires = host.pairingExpiresAt, let selected { if choices.count > 1 { Picker("Connection address", selection: Binding(get: { selected }, set: { qrAddress = $0 })) { ForEach(choices) { item in Text("\(item.label): \(item.address)").tag(item.address) } } .help("Choose the Wi-Fi or VPN address reachable from your Mirror device") } Text("\(selected):\(host.port)").font(.body.monospaced()).textSelection(.enabled) Button(copied ? "Copied" : "Copy Connection Details", systemImage: copied ? "checkmark" : "doc.on.doc") { copyConnectionDetails() } .keyboardShortcut("c", modifiers: .command) .help("Copy the address, port and pairing code together (⌘C)") .accessibilityIdentifier("remote-mirror-copy-connection") MirrorPairingQRCode( address: selected, port: host.port, code: host.pairingKey, expires: expires) Text("In Prowl Mirror on your phone, tap Scan QR Code.") .font(.caption).foregroundStyle(.secondary) } else if selected == nil { Text("No network address is available for pairing. Start Host on a Wi-Fi or VPN interface.") .font(.caption).foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity) }
private var code: some View { VStack(spacing: 8) { if let expires = host.pairingExpiresAt { Text(host.pairingKey) .font(.largeTitle.monospaced().weight(.semibold)) .textSelection(.enabled) .accessibilityIdentifier("remote-mirror-pairing-key") HStack(spacing: 4) { Text("Expires in") Text(expires, style: .timer).monospacedDigit() } .font(.callout).foregroundStyle(.secondary) } else if !host.isRunning && !host.isStarting { Text("Host is off. Start Host to create a pairing code.").foregroundStyle(.secondary) } else if !hasRequestedCode || host.isStarting { ProgressView("Preparing pairing…") } else { Text("The code expired. Refresh to create a new one.").foregroundStyle(.secondary) } } .frame(maxWidth: .infinity) .padding(.vertical, 8) }
private func copyConnectionDetails() { do { guard let address = pairingAddress, let port = UInt16(host.port), let expires = host.pairingExpiresAt else { throw MirrorPairingPayload.Problem.invalid } let payload = try MirrorPairingPayload(address: address, port: port, code: host.pairingKey, expires: expires) let value = try payload.encoded() NSPasteboard.general.clearContents() copied = NSPasteboard.general.setString(value, forType: .string) copyError = copied ? nil : String(localized: "Unable to copy to the clipboard.") } catch { copied = false copyError = error.localizedDescription } }}
private struct ListenOption: Identifiable { let address: String let label: String var id: String { address }}
/// AppKit pop-up button: fills the proposed width, which SwiftUI's menu picker does not.private struct ListenAddressPopUp: NSViewRepresentable { let options: [ListenOption] @Binding var selection: String
func makeNSView(context: Context) -> NSPopUpButton { let button = NSPopUpButton(frame: .zero, pullsDown: false) button.setContentHuggingPriority(.defaultLow, for: .horizontal) button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) button.target = context.coordinator button.action = #selector(Coordinator.changed(_:)) return button }
func updateNSView(_ button: NSPopUpButton, context: Context) { context.coordinator.parent = self if button.itemTitles != options.map(\.label) { button.removeAllItems() for option in options { button.addItem(withTitle: option.label) button.lastItem?.representedObject = option.address } } if let index = options.firstIndex(where: { $0.address == selection }) { button.selectItem(at: index) } }
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
final class Coordinator: NSObject { var parent: ListenAddressPopUp init(parent: ListenAddressPopUp) { self.parent = parent }
@objc func changed(_ sender: NSPopUpButton) { guard let address = sender.selectedItem?.representedObject as? String else { return } parent.selection = address } }}