Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Config/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,13 @@
</array>
</dict>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>Pi Mobile uses the microphone for voice dictation in the chat bar.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Pi Mobile turns your speech into text for chat messages.</string>
<key>NSCameraUsageDescription</key>
<string>Pi Mobile uses the camera to attach photos to messages.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Pi Mobile attaches photos from your library to messages.</string>
</dict>
</plist>
59 changes: 55 additions & 4 deletions PiMobile/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,13 @@ final class APIClient {
return try decoder.decode(T.self, from: data)
}

private func post(_ path: String, body: [String: String] = [:]) async throws -> Data {
private func post(_ path: String, body: some Encodable) async throws -> Data {
guard let mac = activeMac, let url = URL(string: mac.baseURL + path) else { throw APIError.badURL }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(mac.token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Request bodies stay camelCase to match the companion server's JSON.
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) }
Expand All @@ -168,6 +169,10 @@ final class APIClient {
return data
}

private func post(_ path: String) async throws -> Data {
try await post(path, body: [String: String]())
}

func repos(on mac: MacServer) async throws -> [Repo] { try await get("/repos", on: mac) }

func loadModelGroups() async {
Expand All @@ -186,14 +191,60 @@ final class APIClient {
func diff(workspaceId: String) async throws -> WorkspaceDiff { try await get("/workspaces/\(workspaceId)/diff") }
func diffStat(workspaceId: String) async throws -> DiffStat { try await get("/workspaces/\(workspaceId)/diffstat") }

func send(sessionId: String, text: String, model: String? = nil) async throws {
var body = ["text": text]
if let model { body["model"] = model }
struct SendBody: Encodable {
var text: String
var model: String?
var thinking: String?
var approvalMode: String?
var images: [PromptImage]?
}

func send(
sessionId: String,
text: String,
model: String? = nil,
thinking: String? = nil,
approvalMode: String? = nil,
images: [PromptImage]? = nil
) async throws {
let body = SendBody(
text: text,
model: model,
thinking: thinking,
approvalMode: approvalMode,
images: images?.isEmpty == false ? images : nil
)
_ = try await post("/sessions/\(sessionId)/send", body: body)
}

func stop(sessionId: String) async throws { _ = try await post("/sessions/\(sessionId)/stop") }

struct UIResponseBody: Encodable {
var id: String
var confirmed: Bool?
var value: String?
var cancelled: Bool?
}

func respondUI(sessionId: String, id: String, confirmed: Bool? = nil, value: String? = nil, cancelled: Bool? = nil) async throws {
_ = try await post(
"/sessions/\(sessionId)/ui-response",
body: UIResponseBody(id: id, confirmed: confirmed, value: value, cancelled: cancelled)
)
}

struct ApprovalExtensionStatus: Codable {
let installed: Bool
}

func approvalExtensionStatus() async throws -> ApprovalExtensionStatus {
try await get("/approval-extension")
}

func installApprovalExtension() async throws {
_ = try await post("/approval-extension/install")
}

private func delete(_ path: String) async throws {
guard let mac = activeMac, let url = URL(string: mac.baseURL + path) else { throw URLError(.badURL) }
var request = URLRequest(url: url)
Expand Down
35 changes: 35 additions & 0 deletions PiMobile/CameraPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SwiftUI
import UIKit

struct CameraPicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Environment(\.dismiss) private var dismiss

func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = context.coordinator
return picker
}

func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

func makeCoordinator() -> Coordinator { Coordinator(self) }

final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: CameraPicker
init(_ parent: CameraPicker) { self.parent = parent }

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}

func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
parent.image = info[.originalImage] as? UIImage
parent.dismiss()
}
}
}
Loading