diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dcd01d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +# Cancel in-progress runs for the same ref when new commits are pushed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Build & Test (SwiftPM · macOS) + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + # The package declares swift-tools-version 6.2, so it needs a Swift 6.2+ + # toolchain (Xcode 26+). latest-stable tracks that going forward. + - name: Select latest stable Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Show Swift version + run: swift --version + + - name: Resolve dependencies + run: swift package resolve + + - name: Build + run: swift build + + - name: Run tests + run: swift test + + build-example: + name: Build Example App (Xcode · macOS) + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select latest stable Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Show versions + run: | + swift --version + xcodebuild -version + + # Resolve as a distinct step so dependency-resolution failures (the + # BonjourPico / PicoMarkdownView remote packages, or the in-repo + # OpenResponses package) are easy to spot separately from compilation. + - name: Resolve package dependencies + run: > + xcodebuild -resolvePackageDependencies + -project ResponsesExample/ResponsesExample.xcodeproj + -scheme ResponsesExample + -clonedSourcePackagesDirPath .spm + + - name: Build for macOS + run: | + xcodebuild build \ + -project ResponsesExample/ResponsesExample.xcodeproj \ + -scheme ResponsesExample \ + -destination 'platform=macOS' \ + -clonedSourcePackagesDirPath .spm \ + -skipMacroValidation \ + -skipPackagePluginValidation \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" diff --git a/ResponsesExample/ResponsesExample.xcodeproj/project.pbxproj b/ResponsesExample/ResponsesExample.xcodeproj/project.pbxproj index 6c7ce1c..53ccf5e 100644 --- a/ResponsesExample/ResponsesExample.xcodeproj/project.pbxproj +++ b/ResponsesExample/ResponsesExample.xcodeproj/project.pbxproj @@ -8,8 +8,8 @@ /* Begin PBXBuildFile section */ F8B183562E9ECE55007F778C /* PicoMarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = F8B183552E9ECE55007F778C /* PicoMarkdownView */; }; - F8E2A9E02E92E3FC00EFF610 /* PicoResponsesCore in Frameworks */ = {isa = PBXBuildFile; productRef = F8E2A9DF2E92E3FC00EFF610 /* PicoResponsesCore */; }; - F8E2A9E22E92E3FC00EFF610 /* PicoResponsesSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = F8E2A9E12E92E3FC00EFF610 /* PicoResponsesSwiftUI */; }; + F8E2A9E02E92E3FC00EFF610 /* OpenResponses in Frameworks */ = {isa = PBXBuildFile; productRef = F8E2A9DF2E92E3FC00EFF610 /* OpenResponses */; }; + F8E2A9E22E92E3FC00EFF610 /* OpenResponsesSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = F8E2A9E12E92E3FC00EFF610 /* OpenResponsesSwiftUI */; }; F8E2A9F32E94376500EFF610 /* BonjourPico in Frameworks */ = {isa = PBXBuildFile; productRef = F8E2A9F22E94376500EFF610 /* BonjourPico */; }; /* End PBXBuildFile section */ @@ -32,8 +32,8 @@ files = ( F8E2A9F32E94376500EFF610 /* BonjourPico in Frameworks */, F8B183562E9ECE55007F778C /* PicoMarkdownView in Frameworks */, - F8E2A9E22E92E3FC00EFF610 /* PicoResponsesSwiftUI in Frameworks */, - F8E2A9E02E92E3FC00EFF610 /* PicoResponsesCore in Frameworks */, + F8E2A9E22E92E3FC00EFF610 /* OpenResponsesSwiftUI in Frameworks */, + F8E2A9E02E92E3FC00EFF610 /* OpenResponses in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,8 +76,8 @@ ); name = ResponsesExample; packageProductDependencies = ( - F8E2A9DF2E92E3FC00EFF610 /* PicoResponsesCore */, - F8E2A9E12E92E3FC00EFF610 /* PicoResponsesSwiftUI */, + F8E2A9DF2E92E3FC00EFF610 /* OpenResponses */, + F8E2A9E12E92E3FC00EFF610 /* OpenResponsesSwiftUI */, F8E2A9F22E94376500EFF610 /* BonjourPico */, F8B183552E9ECE55007F778C /* PicoMarkdownView */, ); @@ -110,9 +110,9 @@ mainGroup = F8E2A99F2E92D91600EFF610; minimizedProjectReferenceProxies = 1; packageReferences = ( - F8E2A9DE2E92E3FC00EFF610 /* XCLocalSwiftPackageReference "../../PicoResponses" */, + F8E2A9DE2E92E3FC00EFF610 /* XCLocalSwiftPackageReference ".." */, F8E2A9F12E94376500EFF610 /* XCRemoteSwiftPackageReference "BonjourPico" */, - F8B183542E9ECE55007F778C /* XCLocalSwiftPackageReference "../../PicoMarkdownView" */, + F8B183542E9ECE55007F778C /* XCRemoteSwiftPackageReference "PicoMarkdownView" */, ); preferredProjectObjectVersion = 77; productRefGroup = F8E2A9A92E92D91600EFF610 /* Products */; @@ -393,23 +393,27 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - F8B183542E9ECE55007F778C /* XCLocalSwiftPackageReference "../../PicoMarkdownView" */ = { + F8E2A9DE2E92E3FC00EFF610 /* XCLocalSwiftPackageReference ".." */ = { isa = XCLocalSwiftPackageReference; - relativePath = ../../PicoMarkdownView; - }; - F8E2A9DE2E92E3FC00EFF610 /* XCLocalSwiftPackageReference "../../PicoResponses" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../../PicoResponses; + relativePath = ..; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */ + F8B183542E9ECE55007F778C /* XCRemoteSwiftPackageReference "PicoMarkdownView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PicoMLX/PicoMarkdownView"; + requirement = { + branch = main; + kind = branch; + }; + }; F8E2A9F12E94376500EFF610 /* XCRemoteSwiftPackageReference "BonjourPico" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/PicoMLX/BonjourPico"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.1; + branch = main; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -417,15 +421,16 @@ /* Begin XCSwiftPackageProductDependency section */ F8B183552E9ECE55007F778C /* PicoMarkdownView */ = { isa = XCSwiftPackageProductDependency; + package = F8B183542E9ECE55007F778C /* XCRemoteSwiftPackageReference "PicoMarkdownView" */; productName = PicoMarkdownView; }; - F8E2A9DF2E92E3FC00EFF610 /* PicoResponsesCore */ = { + F8E2A9DF2E92E3FC00EFF610 /* OpenResponses */ = { isa = XCSwiftPackageProductDependency; - productName = PicoResponsesCore; + productName = OpenResponses; }; - F8E2A9E12E92E3FC00EFF610 /* PicoResponsesSwiftUI */ = { + F8E2A9E12E92E3FC00EFF610 /* OpenResponsesSwiftUI */ = { isa = XCSwiftPackageProductDependency; - productName = PicoResponsesSwiftUI; + productName = OpenResponsesSwiftUI; }; F8E2A9F22E94376500EFF610 /* BonjourPico */ = { isa = XCSwiftPackageProductDependency; diff --git a/ResponsesExample/ResponsesExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ResponsesExample/ResponsesExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8d04352..852beaf 100644 --- a/ResponsesExample/ResponsesExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ResponsesExample/ResponsesExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PicoMLX/BonjourPico", "state" : { - "revision" : "e8b8c906d4c0e5da34d6e81a82d4cdd62a7c0d5b", - "version" : "0.0.1" + "branch" : "main", + "revision" : "c8dece524057da6f275d7aed9af8cbaa3c057a65" } }, { @@ -19,6 +19,15 @@ "version" : "1.3.0" } }, + { + "identity" : "picomarkdownview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PicoMLX/PicoMarkdownView", + "state" : { + "branch" : "main", + "revision" : "afc6e54749ba5960a124fc7918c16b866df35696" + } + }, { "identity" : "swiftmath", "kind" : "remoteSourceControl", diff --git a/ResponsesExample/ResponsesExample.xcodeproj/xcshareddata/xcschemes/ResponsesExample.xcscheme b/ResponsesExample/ResponsesExample.xcodeproj/xcshareddata/xcschemes/ResponsesExample.xcscheme new file mode 100644 index 0000000..b7948fc --- /dev/null +++ b/ResponsesExample/ResponsesExample.xcodeproj/xcshareddata/xcschemes/ResponsesExample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ResponsesExample/ResponsesExample/SelectServerView.swift b/ResponsesExample/ResponsesExample/SelectServerView.swift index a831a85..b7f20b7 100644 --- a/ResponsesExample/ResponsesExample/SelectServerView.swift +++ b/ResponsesExample/ResponsesExample/SelectServerView.swift @@ -13,12 +13,16 @@ struct SelectServerView: View { @State var bonjourPico = BonjourPico() @Binding var server: (URL, String?, [String])? + + // Local snapshot of discovered servers. Mirrors `bonjourPico.endpoints` while scanning, + // but is kept (not cleared) when the user stops scanning so the list stays selectable. + @State private var discoveredServers: [BonjourEndpoint] = [] var body: some View { VStack { List { Section("Local Pico AI Servers") { - if bonjourPico.servers.isEmpty { + if discoveredServers.isEmpty { VStack(alignment: .leading) { Text("No Pico AI servers found on this network") Group { @@ -29,10 +33,17 @@ struct SelectServerView: View { .foregroundStyle(.secondary) } } - ForEach(bonjourPico.servers, id: \.self) { server in - Button("\(server.name)") { - guard let baseURL = URL(string: "http://\(server.ipAddress):\(server.port)") else { - print("Invalid url: http://\(server.ipAddress):\(server.port)") + ForEach(discoveredServers) { endpoint in + Button("\(endpoint.displayName)") { + // Prefer the Bonjour host name (recommended for connecting); fall back + // to an advertised IP. Bracket IPv6 literals so the URL host stays valid. + guard let rawHost = endpoint.hostName ?? endpoint.ipAddresses.first else { + print("Invalid url for \(endpoint.displayName)") + return + } + let host = rawHost.contains(":") ? "[\(rawHost)]" : rawHost + guard let baseURL = URL(string: "http://\(host):\(endpoint.port)") else { + print("Invalid url for \(endpoint.displayName)") return } let apiRoot = baseURL.appendingPathComponent("v1") @@ -66,13 +77,34 @@ struct SelectServerView: View { .buttonStyle(.plain) Button(bonjourPico.isScanning ? "Stop scanning for Pico AI Servers" : "Scan for Pico AI Servers") { - bonjourPico.startStop() + Task { + if bonjourPico.isScanning { + await bonjourPico.stopScanning() + } else { + await startScan() + } + } } } .padding() - .onAppear { - bonjourPico.startStop() + .task { + await startScan() } + .onChange(of: bonjourPico.endpoints) { _, endpoints in + // Mirror live results while scanning. stopScanning() sets isScanning = false + // before clearing endpoints, so the cleared snapshot is ignored here and the + // last discovered list stays visible (and selectable) after the scan stops. + if bonjourPico.isScanning { + discoveredServers = endpoints + } + } + } + + /// Starts a fresh scan. Clears the kept snapshot first so a new scan that finds nothing + /// doesn't leave stale servers from a previous (stopped) scan visible and selectable. + private func startScan() async { + discoveredServers = [] + try? await bonjourPico.startScanning() } }