2026-10-18 22:17:54 -07:00
import ByreAPI
2026-10-18 16:14:03 -07:00
import Foundation
import SwiftUI
2026-10-18 22:17:54 -07:00
typealias Permission = Components . Schemas . Permission
2026-10-19 15:08:57 -07:00
typealias UsageCommands = Components . Schemas . UsageCommands
2026-10-18 22:17:54 -07:00
2026-10-18 16:14:03 -07:00
let serverHelper = Bundle . main . bundleURL . appending ( path : "Contents/Helpers/byre" ). path
private let service = "gui/ \( getuid ()) /run.fc.byre"
struct ServerState {
var on = false
var address : String ?
}
2026-10-18 22:17:54 -07:00
struct Check : Decodable {
let name : String
let need : String
let ` for `: String
let ok : Bool
2026-10-19 11:03:31 -07:00
let command : String
2026-10-18 22:17:54 -07:00
let fix : String
let blocking : Bool
}
struct Checks : Decodable {
let ready : Bool
let checks : [ Check ]
}
struct ChecksView : View {
let checks : [ Check ]
var body : some View {
VStack ( alignment : . leading , spacing : 10 ) {
ForEach ( checks , id : \ . name ) { check in
VStack ( alignment : . leading , spacing : 2 ) {
HStack ( spacing : 8 ) {
Text ( check . name ). font ( Fonts . font ( 13 , weight : 700 ))
status ( check )
}
Text ( " \( need ( check )) , for \( check . for ) " ). foregroundStyle ( Palette . muted )
if ! check . ok {
2026-10-19 11:03:31 -07:00
if ! check . command . isEmpty {
CommandBox ( command : check . command )
}
if ! check . fix . isEmpty {
Text ( check . fix )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
}
2026-10-18 22:17:54 -07:00
}
}
}
}
}
private func status ( _ check : Check ) -> some View {
if check . ok {
return Text ( "Found" ). foregroundStyle ( Palette . ok )
}
return Text ( "Missing" ). foregroundStyle ( check . blocking ? Palette . danger : Palette . muted )
}
private func need ( _ check : Check ) -> String {
switch check . need {
case "required" :
return "Required"
case "agent" :
return "One of claude, codex and agy required"
default :
return "Optional"
}
}
}
2026-10-19 11:03:31 -07:00
private struct CommandBox : View {
let command : String
@ ViewState private var copied = false
@ ViewState private var failure : String ?
var body : some View {
box
if let failure {
Text ( failure )
. foregroundStyle ( Palette . danger )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
}
}
private var box : some View {
HStack ( alignment : . firstTextBaseline , spacing : 8 ) {
Text ( verbatim : command )
. font ( Fonts . font ( 12 ))
. textSelection (. enabled )
. frame ( maxWidth : . infinity , alignment : . leading )
Button ( copied ? "Copied" : "Copy" , action : copy )
. buttonStyle ( SmallLinkButtonStyle ())
. help ( "Copy the command to the clipboard" )
Button ( "Run in Terminal" , action : run )
. buttonStyle ( SmallLinkButtonStyle ())
. help ( "Open Terminal and run the command" )
}
. padding (. horizontal , 8 )
. padding (. vertical , 6 )
. background ( Palette . raised , in : RoundedRectangle ( cornerRadius : 6 ))
. overlay ( RoundedRectangle ( cornerRadius : 6 ). stroke ( Palette . border ))
. padding (. vertical , 2 )
}
private func copy () {
NSPasteboard . general . clearContents ()
NSPasteboard . general . setString ( command , forType : . string )
copied = true
Task {
try ? await Task . sleep ( for : . milliseconds ( 1500 ))
copied = false
}
}
private func run () {
let shell = unsafe String ( cString : getpwuid ( getuid ()). pointee . pw_shell )
let file = FileManager . default . temporaryDirectory . appending ( path : "byre-install- \( UUID () . uuidString ) .command" )
do {
try "#! \( shell ) -l \n \( command ) \n " . write ( to : file , atomically : true , encoding : . utf8 )
try FileManager . default . setAttributes ([. posixPermissions : 0o755 ], ofItemAtPath : file . path )
failure = nil
} catch {
failure = errorText ( error )
return
}
NSWorkspace . shared . open ([ file ], withApplicationAt : URL ( filePath : "/System/Applications/Utilities/Terminal.app" ), configuration : NSWorkspace . OpenConfiguration ())
}
}
2026-10-18 22:17:54 -07:00
// replayd keys ScreenCaptureKit clients by executable path, so the check runs from its own copy beside client_hands' byre-hands.
func checkLocalPermissions () async throws -> [ Permission ] {
let dir = FileManager . default . temporaryDirectory . appending ( path : "byre-hands-check- \( UUID () . uuidString ) " )
try FileManager . default . createDirectory ( at : dir , withIntermediateDirectories : true )
defer {
try ? FileManager . default . removeItem ( at : dir )
}
let bin = dir . appending ( path : "byre-hands" )
try FileManager . default . copyItem ( at : Bundle . main . bundleURL . appending ( path : "Contents/Helpers/byre-hands" ), to : bin )
let r = try await execute ( bin . path , [ "check" ])
guard r . status == 0 , let perms = try ? JSONDecoder (). decode ([ Permission ]. self , from : Data ( r . output . utf8 )) else {
throw ProcessFailure ( errorDescription : "byre-hands check failed: \( r . output . trimmingCharacters ( in : . whitespacesAndNewlines )) " )
}
return perms
}
struct PermissionsCheck : View {
let run : () async throws -> [ Permission ]
@ ViewState private var result : Result < [ Permission ], any Error >?
@ ViewState private var busy = false
var body : some View {
VStack ( alignment : . leading , spacing : 10 ) {
Button ( busy ? "Checking…" : "Check permissions" , action : check )
. buttonStyle ( WebButtonStyle ())
. disabled ( busy )
switch result {
case . success ( let perms ):
ForEach ( perms , id : \ . name ) { perm in
row ( perm )
}
case . failure ( let error ):
Text ( errorText ( error ))
. foregroundStyle ( Palette . danger )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
case nil :
EmptyView ()
}
}
. padding (. top , 4 )
}
private func row ( _ perm : Permission ) -> some View {
VStack ( alignment : . leading , spacing : 2 ) {
HStack ( spacing : 8 ) {
Text ( perm . name ). font ( Fonts . font ( 13 , weight : 700 ))
switch perm . status {
case . granted :
Text ( "Granted" ). foregroundStyle ( Palette . ok )
case . missing :
Text ( "Missing" ). foregroundStyle ( Palette . danger )
case . unchecked :
Text ( "Not checked" ). foregroundStyle ( Palette . muted )
}
}
Text ( perm . detail )
. foregroundStyle ( Palette . muted )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
if perm . status == . missing , let url = URL ( string : perm . settings ) {
Button ( "Open System Settings" ) {
NSWorkspace . shared . open ( url )
}
. buttonStyle ( SmallLinkButtonStyle ())
}
}
}
private func check () {
busy = true
Task {
do {
result = . success ( try await run ())
} catch {
result = . failure ( error )
}
busy = false
}
}
}
2026-10-19 15:08:57 -07:00
private struct UsageCommandsForm : View {
let api : API
@ ViewState private var saved : UsageCommands ?
@ ViewState private var draft : UsageCommands ?
@ ViewState private var error = ""
@ ViewState private var busy = false
var body : some View {
VStack ( alignment : . leading , spacing : 8 ) {
if draft != nil {
Grid ( alignment : . leading , horizontalSpacing : 12 , verticalSpacing : 6 ) {
row ( "claude" , \ . claude )
row ( "codex" , \ . codex )
row ( "antigravity" , \ . antigravity )
}
Button ( busy ? "Saving…" : "Save" , action : save )
. buttonStyle ( WebButtonStyle ())
. disabled ( busy || draft == saved )
}
if ! error . isEmpty {
Text ( error )
. foregroundStyle ( Palette . danger )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
}
}
. padding (. top , 4 )
. task {
do {
saved = try await api . client . getUsageCommands (). ok . body . json
draft = saved
error = ""
} catch {
self . error = "Couldn't load usage commands: \( errorText ( error )) "
}
}
}
private func row ( _ vendor : String , _ key : WritableKeyPath < UsageCommands , String >) -> some View {
GridRow {
Text ( vendor )
TextField ( "Built in" , text : Binding ( get : { draft ?[ keyPath : key ] ?? "" }, set : { draft ?[ keyPath : key ] = $0 }))
. modifier ( FieldStyle ())
. onSubmit ( save )
}
}
private func save () {
guard let draft , draft != saved , ! busy else {
return
}
busy = true
Task {
do {
saved = try await api . client . updateUsageCommands ( body : . json ( draft )). ok . body . json
self . draft = saved
error = ""
} catch {
self . error = "Couldn't save usage commands: \( errorText ( error )) "
}
busy = false
}
}
}
2026-10-18 16:14:03 -07:00
struct ProcessFailure : LocalizedError {
let errorDescription : String ?
}
private func execute ( _ path : String , _ args : [ String ], env : [ String : String ] = [:]) async throws -> ( status : Int32 , output : String ) {
try await Task . detached {
let p = Process ()
p . executableURL = URL ( filePath : path )
p . arguments = args
p . environment = ProcessInfo . processInfo . environment . merging ( env ) { $1 }
let out = Pipe ()
p . standardOutput = out
p . standardError = out
try p . run ()
let data = out . fileHandleForReading . readDataToEndOfFile ()
p . waitUntilExit ()
return ( p . terminationStatus , String ( decoding : data , as : UTF8 . self ))
}. value
}
private func launchctl ( _ args : String ...) async throws {
let r = try await execute ( "/bin/launchctl" , args )
if r . status != 0 {
throw ProcessFailure ( errorDescription : "launchctl \( args . joined ( separator : " " )) : \( r . output . trimmingCharacters ( in : . whitespacesAndNewlines )) " )
}
}
enum Server {
static func state () async -> ServerState {
guard let r = try ? await execute ( "/bin/launchctl" , [ "print" , service ]), r . status == 0 else {
return ServerState ()
}
return ServerState ( on : true , address : await address ())
}
private static func address () async -> String ? {
guard let r = try ? await execute ( "/Applications/Tailscale.app/Contents/MacOS/Tailscale" , [ "serve" , "status" , "--json" ], env : [ "TAILSCALE_BE_CLI" : "1" ]), r . status == 0 ,
let json = try ? JSONSerialization . jsonObject ( with : Data ( r . output . utf8 )) as ? [ String : Any ],
let web = json [ "Web" ] as ? [ String : Any ],
let host = web . keys . first ( where : { $0 . hasSuffix ( ":443" ) })
else {
return nil
}
return "https://" + host . dropLast ( 4 )
}
// byre install copies its PATH into the service, which needs the login shell's to find claude, codex, agy and npx.
2026-10-18 22:17:54 -07:00
private static func helper ( _ command : String ) async throws -> ( status : Int32 , output : String ) {
2026-10-18 16:14:03 -07:00
let shell = unsafe String ( cString : getpwuid ( getuid ()). pointee . pw_shell )
2026-10-18 22:17:54 -07:00
return try await execute ( shell , [ "-l" , "-c" , "exec \" $BYRE_HELPER \" \( command ) " ], env : [ "BYRE_HELPER" : serverHelper ])
}
static func checks () async throws -> Checks {
let r = try await helper ( "check" )
guard r . status == 0 , let line = r . output . split ( separator : " \n " ). last , let checks = try ? JSONDecoder (). decode ( Checks . self , from : Data ( line . utf8 )) else {
throw ProcessFailure ( errorDescription : "byre check failed: \( r . output . trimmingCharacters ( in : . whitespacesAndNewlines )) " )
}
return checks
}
static func install () async throws -> URL {
let r = try await helper ( "install" )
2026-10-18 16:14:03 -07:00
let prefix = "byre is at "
guard r . status == 0 , let line = r . output . split ( separator : " \n " ). first ( where : { $0 . hasPrefix ( prefix ) }), let url = URL ( string : String ( line . dropFirst ( prefix . count ))) else {
throw ProcessFailure ( errorDescription : "byre install failed: \( r . output . trimmingCharacters ( in : . whitespacesAndNewlines )) " )
}
return url
}
// Reinstalling a running service restarts it, so an address read right after could still be answered by the old one.
static func turnOn () async throws -> URL {
let state = await state ()
if state . on , let address = state . address , let url = URL ( string : address ) {
return url
}
return try await install ()
}
static func turnOff () async throws {
try await launchctl ( "bootout" , service )
}
}
struct ServerView : View {
@ Environment ( Overlays . self ) private var overlays
@ ViewState private var state : ServerState ?
2026-10-18 22:17:54 -07:00
@ ViewState private var checks : Result < Checks , any Error >?
2026-10-18 16:14:03 -07:00
@ ViewState private var busy = false
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : 8 ) {
2026-10-19 12:05:52 -07:00
Text ( "Server on \( macName ) " )
2026-10-18 16:14:03 -07:00
. font ( Fonts . font ( 13 , weight : 700 ))
. padding (. top , 16 )
if let state {
details ( state )
}
2026-10-19 12:05:52 -07:00
Text ( "Tools on \( macName ) " )
2026-10-18 22:17:54 -07:00
. font ( Fonts . font ( 13 , weight : 700 ))
. padding (. top , 16 )
switch checks {
case . success ( let checks ):
ChecksView ( checks : checks . checks )
case . failure ( let error ):
Text ( errorText ( error ))
. foregroundStyle ( Palette . danger )
. textSelection (. enabled )
case nil :
EmptyView ()
}
if let address = state ?. address , let origin = URL ( string : address ) {
Text ( "Agents' permissions" )
. font ( Fonts . font ( 13 , weight : 700 ))
. padding (. top , 16 )
Text ( "The macOS privacy grants the server's agents use to see and control this Mac. Checking asks macOS for any that are missing." )
. foregroundStyle ( Palette . muted )
. fixedSize ( horizontal : false , vertical : true )
PermissionsCheck {
try await API ( origin : origin ). client . checkPermissions (). ok . body . json
}
2026-10-19 15:08:57 -07:00
Text ( "Usage commands" )
. font ( Fonts . font ( 13 , weight : 700 ))
. padding (. top , 16 )
Text ( "A shell command per vendor that prints its usage as JSON for the Status tab, run after each of that vendor's turns. Empty uses what the vendor itself reports." )
. foregroundStyle ( Palette . muted )
. fixedSize ( horizontal : false , vertical : true )
UsageCommandsForm ( api : API ( origin : origin ))
2026-10-18 22:17:54 -07:00
}
2026-10-18 16:14:03 -07:00
}
. padding (. horizontal , 16 )
. padding (. top , 8 )
. padding (. bottom , 16 )
. frame ( maxWidth : . infinity , alignment : . leading )
}
. frame ( maxWidth : . infinity , maxHeight : . infinity )
. background ( Palette . bg )
. task {
await refresh ()
}
. onReceive ( NotificationCenter . default . publisher ( for : NSApplication . didBecomeActiveNotification )) { _ in
Task {
await refresh ()
}
}
}
@ ViewBuilder
private func details ( _ state : ServerState ) -> some View {
if state . on {
Text ( "On" ). foregroundStyle ( Palette . ok )
if let address = state . address {
Text ( address ). textSelection (. enabled )
}
button ( "Turn off" , Server . turnOff )
} else {
Text ( "Off" ). foregroundStyle ( Palette . muted )
button ( "Turn on" ) {
_ = try await Server . turnOn ()
}
}
}
private func button ( _ label : String , _ action : @ escaping () async throws -> Void ) -> some View {
Button ( label ) {
busy = true
Task {
do {
try await action ()
} catch {
overlays . showAlert ( errorText ( error ))
}
await refresh ()
busy = false
}
}
. buttonStyle ( WebButtonStyle ())
. disabled ( busy )
. padding (. top , 4 )
}
private func refresh () async {
2026-10-18 22:17:54 -07:00
async let checked = check ()
2026-10-18 16:14:03 -07:00
state = await Server . state ()
2026-10-18 22:17:54 -07:00
checks = await checked
}
private func check () async -> Result < Checks , any Error > {
do {
return . success ( try await Server . checks ())
} catch {
return . failure ( error )
}
2026-10-18 16:14:03 -07:00
}
}