Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ GitHub: [velos/CodeMode.swift](https://github.com/velos/CodeMode.swift)
## Highlights

- Platforms: `iOS 18+`, `macOS 15+`, `visionOS 2+`
- watchOS is audited but not a runtime target in this package because the current runtime depends on JavaScriptCore.
- Typed Swift host API through `CodeModeAgentTools`
- Streaming execution via `JavaScriptExecutionCall`
- Structured failures via `CodeModeToolError`
- Hybrid JS surface:
- web-style globals: `fetch`, `URL`, `URLSearchParams`, `setTimeout`, `console`
- cross-platform Apple namespaces: `apple.keychain`, `apple.location`, `apple.weather`, `apple.calendar`, `apple.reminders`, `apple.contacts`, `apple.photos`, `apple.vision`, `apple.notifications`, `apple.health`, `apple.home`, `apple.media`, `apple.fs`
- platform-specific namespaces when needed: `ios.alarm`
- iOS/visionOS system UI helpers through an injected presenter, including alerts, calendar editors, photo/contact/document pickers, share sheets, Quick Look previews, web authentication, and iOS-only camera/scan/mail/message compose flows
- Node-style aliases for file operations through `globalThis.fs.promises`
- Sandboxed filesystem policy with allowed roots: `tmp`, `caches`, `documents`
- Search and execution only expose helpers supported on the current host platform
Expand Down Expand Up @@ -50,6 +52,8 @@ Then add the product to your target:
- `CodeModeFileSystem`
- `LocalCodeModeFileSystem`
- `CodeModeAgentToolDescriptions`
- `SystemUIPresenter`
- `UIKitSystemUIPresenter` on iOS/visionOS

## Quick Start

Expand Down Expand Up @@ -189,6 +193,8 @@ Cross-platform privileged helpers are installed under `apple.*`. Platform-specif

`apple.location.requestPermission()` is currently exposed only on iOS hosts. Other Apple platforms can expose `apple.location.*` helpers when supported, but the explicit permission-request helper is intentionally hidden outside iOS for now.

System UI helpers are installed only on supported UI platforms. Shared iOS/visionOS helpers include `apple.ui.presentAlert`, `apple.ui.presentPrompt`, `apple.settings.open`, `apple.calendar.pickCalendar`, `apple.calendar.presentEvent`, `apple.calendar.presentNewEvent`, `apple.contacts.pick`, `apple.contacts.presentContact`, `apple.contacts.presentNewContact`, `apple.photos.pick`, `apple.photos.presentLimitedLibraryPicker`, `apple.documents.pick`, `apple.documents.export`, `apple.documents.save`, `apple.documents.openIn`, `apple.share.present`, `apple.quicklook.preview`, `apple.camera.scanData`, `apple.print.present`, `apple.web.present`, and `apple.auth.webAuthenticate`. iOS-only helpers include `apple.camera.capture`, `apple.documents.scan`, `apple.mail.compose`, and `apple.messages.compose`. Host apps must provide a `SystemUIPresenter`; otherwise UI-presenting helpers fail with `UI_PRESENTER_UNAVAILABLE`.

`call.events` is a non-throwing `AsyncStream` that can emit:

- `.log(ExecutionLog)`
Expand Down Expand Up @@ -228,11 +234,15 @@ Required Info.plist keys by capability:

- Location read (`location.read`): `NSLocationWhenInUseUsageDescription`
- Location permission request (`location.permission.request`, iOS-only): `NSLocationWhenInUseUsageDescription`
- Contacts (`contacts.read`, `contacts.search`): `NSContactsUsageDescription`
- Contacts (`contacts.read`, `contacts.search`, `contacts.ui.presentContact`, `contacts.ui.presentNewContact`): `NSContactsUsageDescription`
- Calendar read (`calendar.read`): `NSCalendarsFullAccessUsageDescription`
- Calendar event detail UI (`calendar.ui.presentEvent`): `NSCalendarsFullAccessUsageDescription`
- Calendar write-only (`calendar.write`): `NSCalendarsWriteOnlyAccessUsageDescription`
- Calendar event editor/chooser UI (`calendar.ui.presentNewEvent`, `calendar.ui.pickCalendar`): `NSCalendarsWriteOnlyAccessUsageDescription`
- Reminders (`reminders.read`, `reminders.write`): `NSRemindersFullAccessUsageDescription`
- Photos (`photos.read`, `photos.export`): `NSPhotoLibraryUsageDescription`
- Photos (`photos.read`, `photos.export`, `photos.ui.presentLimitedLibraryPicker`): `NSPhotoLibraryUsageDescription`
- Camera UI (`camera.ui.capture`, `camera.ui.scanData`, `documents.ui.scan`): `NSCameraUsageDescription`
- Camera video capture (`camera.ui.capture`): `NSMicrophoneUsageDescription`
- AlarmKit (`alarm.permission.request`, `alarm.read`, `alarm.schedule`, `alarm.cancel`): `NSAlarmKitUsageDescription`
- HealthKit read (`health.permission.request`, `health.read`): `NSHealthShareUsageDescription`
- HealthKit write (`health.permission.request`, `health.write`): `NSHealthUpdateUsageDescription`
Expand Down Expand Up @@ -284,7 +294,7 @@ swift run --package-path Tools/CodeModeEval codemode-eval run fs.round-trip --sh
swift run --package-path Tools/CodeModeEval codemode-eval run --json
```

The eval harness runs 21 built-in user-style scenarios through the same
The eval harness runs 26 built-in user-style scenarios through the same
`searchJavaScriptAPI` and `executeJavaScript` APIs that host apps expose to
agents. It validates tool order, discovered catalog output, generated JavaScript
fragments, exact `allowedCapabilities`, structured errors, repair suggestions,
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodeMode/API/BridgeErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum BridgeError: Error, Sendable {
case capabilityNotFound(String)
case permissionDenied(PermissionKind)
case unsupportedPlatform(String)
case uiPresenterUnavailable
case timeout(milliseconds: Int)
case cancelled
case pathViolation(String)
Expand All @@ -29,6 +30,8 @@ extension BridgeError: LocalizedError {
return "Permission denied: \(permission.rawValue)"
case let .unsupportedPlatform(feature):
return "Unsupported platform for \(feature)"
case .uiPresenterUnavailable:
return "System UI presenter is unavailable. Configure CodeModeConfiguration.systemUIPresenter before using UI-presenting helpers."
case let .timeout(milliseconds):
return "Execution timed out after \(milliseconds)ms"
case .cancelled:
Expand Down Expand Up @@ -56,6 +59,8 @@ extension BridgeError: LocalizedError {
return "PERMISSION_DENIED"
case .unsupportedPlatform:
return "UNSUPPORTED_PLATFORM"
case .uiPresenterUnavailable:
return "UI_PRESENTER_UNAVAILABLE"
case .timeout:
return "EXECUTION_TIMEOUT"
case .cancelled:
Expand Down
33 changes: 32 additions & 1 deletion Sources/CodeMode/API/BridgeModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@ public struct CodeModeConfiguration: Sendable {
public var artifactStore: any ArtifactStore
public var permissionBroker: any PermissionBroker
public var auditLogger: any AuditLogger
public var systemUIPresenter: any SystemUIPresenter
public var hostPlatform: HostPlatform

public init(
pathPolicy: any PathPolicy = DefaultPathPolicy(),
fileSystem: any CodeModeFileSystem = LocalCodeModeFileSystem(),
artifactStore: any ArtifactStore = InMemoryArtifactStore(),
permissionBroker: any PermissionBroker = SystemPermissionBroker(),
auditLogger: any AuditLogger = SyncAuditLogger()
auditLogger: any AuditLogger = SyncAuditLogger(),
systemUIPresenter: any SystemUIPresenter = UnavailableSystemUIPresenter(),
hostPlatform: HostPlatform = .current
) {
self.pathPolicy = pathPolicy
self.fileSystem = fileSystem
self.artifactStore = artifactStore
self.permissionBroker = permissionBroker
self.auditLogger = auditLogger
self.systemUIPresenter = systemUIPresenter
self.hostPlatform = hostPlatform
}
}

Expand Down Expand Up @@ -318,15 +324,19 @@ public enum CapabilityID: String, Sendable, Codable, CaseIterable, Hashable {

case calendarRead = "calendar.read"
case calendarWrite = "calendar.write"
case calendarUIPresentNewEvent = "calendar.ui.presentNewEvent"

case remindersRead = "reminders.read"
case remindersWrite = "reminders.write"

case contactsRead = "contacts.read"
case contactsSearch = "contacts.search"
case contactsUIPick = "contacts.ui.pick"

case photosRead = "photos.read"
case photosExport = "photos.export"
case photosUIPick = "photos.ui.pick"
case photosUIPresentLimitedLibraryPicker = "photos.ui.presentLimitedLibraryPicker"

case visionImageAnalyze = "vision.image.analyze"

Expand Down Expand Up @@ -361,4 +371,25 @@ public enum CapabilityID: String, Sendable, Codable, CaseIterable, Hashable {
case fsMkdir = "fs.mkdir"
case fsExists = "fs.exists"
case fsAccess = "fs.access"

case calendarUIPickCalendar = "calendar.ui.pickCalendar"
case calendarUIPresentEvent = "calendar.ui.presentEvent"
case contactsUIPresentContact = "contacts.ui.presentContact"
case contactsUIPresentNewContact = "contacts.ui.presentNewContact"
case documentsUIPick = "documents.ui.pick"
case documentsUIExport = "documents.ui.export"
case documentsUIOpenIn = "documents.ui.openIn"
case documentsUIScan = "documents.ui.scan"
case shareUIPresent = "share.ui.present"
case quickLookUIPreview = "quicklook.ui.preview"
case cameraUICapture = "camera.ui.capture"
case cameraUIScanData = "camera.ui.scanData"
case mailUICompose = "mail.ui.compose"
case messagesUICompose = "messages.ui.compose"
case printUIPresent = "print.ui.present"
case webUIPresent = "web.ui.present"
case authUIWebAuthenticate = "auth.ui.webAuthenticate"
case uiAlertPresent = "ui.alert.present"
case uiPromptPresent = "ui.prompt.present"
case settingsUIOpen = "settings.ui.open"
}
178 changes: 178 additions & 0 deletions Sources/CodeMode/API/SystemUIPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import Foundation

public protocol SystemUIPresenter: Sendable {
func pickCalendar(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentCalendarEvent(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentNewCalendarEvent(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func pickPhotos(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func pickContacts(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentContact(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentNewContact(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func pickDocuments(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func exportDocuments(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func openDocument(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func scanDocuments(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentShareSheet(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func previewQuickLook(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func captureCamera(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func scanData(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func composeMail(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func composeMessage(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentPrint(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentWeb(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func authenticateWeb(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentAlert(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentPrompt(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func presentLimitedPhotoLibraryPicker(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
func openSettings(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue
}

public extension SystemUIPresenter {
func pickCalendar(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentCalendarEvent(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentNewCalendarEvent(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func pickPhotos(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func pickContacts(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentContact(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentNewContact(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func pickDocuments(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func exportDocuments(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func openDocument(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func scanDocuments(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentShareSheet(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func previewQuickLook(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func captureCamera(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func scanData(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func composeMail(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func composeMessage(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentPrint(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentWeb(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func authenticateWeb(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentAlert(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentPrompt(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func presentLimitedPhotoLibraryPicker(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}

func openSettings(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
_ = arguments
_ = context
throw BridgeError.uiPresenterUnavailable
}
}

public struct UnavailableSystemUIPresenter: SystemUIPresenter {
public init() {}
}
Loading
Loading